Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/cli-reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This documentation is auto-generated from test cases.
| Category | Options | Description |
|----------|---------|-------------|
| 📁 [Base Options](base-options.md) | 5 | Input/output configuration |
| 🔧 [Typing Customization](typing-customization.md) | 19 | Type annotation and import behavior |
| 🔧 [Typing Customization](typing-customization.md) | 20 | Type annotation and import behavior |
| 🏷️ [Field Customization](field-customization.md) | 21 | Field naming and docstring behavior |
| 🏗️ [Model Customization](model-customization.md) | 30 | Model generation behavior |
| 🎨 [Template Customization](template-customization.md) | 16 | Output formatting and custom rendering |
Expand Down Expand Up @@ -184,6 +184,7 @@ This documentation is auto-generated from test cases.
- [`--use-status-code-in-response-name`](openapi-only-options.md#use-status-code-in-response-name)
- [`--use-subclass-enum`](model-customization.md#use-subclass-enum)
- [`--use-title-as-name`](field-customization.md#use-title-as-name)
- [`--use-tuple-for-fixed-items`](typing-customization.md#use-tuple-for-fixed-items)
- [`--use-type-alias`](typing-customization.md#use-type-alias)
- [`--use-unique-items-as-set`](typing-customization.md#use-unique-items-as-set)

Expand Down
2 changes: 2 additions & 0 deletions docs/cli-reference/quick-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ datamodel-codegen [OPTIONS]
| [`--use-non-positive-negative-number-constrained-types`](typing-customization.md#use-non-positive-negative-number-constrained-types) | Use NonPositive/NonNegative types for number constraints. |
| [`--use-pendulum`](typing-customization.md#use-pendulum) | Use pendulum types for date/time fields instead of datetime module. |
| [`--use-standard-primitive-types`](typing-customization.md#use-standard-primitive-types) | Use Python standard library types for string formats instead of str. |
| [`--use-tuple-for-fixed-items`](typing-customization.md#use-tuple-for-fixed-items) | Generate tuple types for arrays with items array syntax. |
| [`--use-type-alias`](typing-customization.md#use-type-alias) | Use TypeAlias instead of root models for type definitions (experimental). |
| [`--use-unique-items-as-set`](typing-customization.md#use-unique-items-as-set) | Generate set types for arrays with uniqueItems constraint. |

Expand Down Expand Up @@ -283,6 +284,7 @@ All options sorted alphabetically:
- [`--use-status-code-in-response-name`](openapi-only-options.md#use-status-code-in-response-name) - Include HTTP status code in response model names.
- [`--use-subclass-enum`](model-customization.md#use-subclass-enum) - Generate typed Enum subclasses for enums with specific field...
- [`--use-title-as-name`](field-customization.md#use-title-as-name) - Use schema title as the generated class name.
- [`--use-tuple-for-fixed-items`](typing-customization.md#use-tuple-for-fixed-items) - Generate tuple types for arrays with items array syntax.
- [`--use-type-alias`](typing-customization.md#use-type-alias) - Use TypeAlias instead of root models for type definitions (e...
- [`--use-unique-items-as-set`](typing-customization.md#use-unique-items-as-set) - Generate set types for arrays with uniqueItems constraint.
- [`--validation`](openapi-only-options.md#validation) - Enable validation constraints (deprecated, use --field-const...
Expand Down
54 changes: 54 additions & 0 deletions docs/cli-reference/typing-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
| [`--use-non-positive-negative-number-constrained-types`](#use-non-positive-negative-number-constrained-types) | Use NonPositive/NonNegative types for number constraints. |
| [`--use-pendulum`](#use-pendulum) | Use pendulum types for date/time fields instead of datetime ... |
| [`--use-standard-primitive-types`](#use-standard-primitive-types) | Use Python standard library types for string formats instead... |
| [`--use-tuple-for-fixed-items`](#use-tuple-for-fixed-items) | Generate tuple types for arrays with items array syntax. |
| [`--use-type-alias`](#use-type-alias) | Use TypeAlias instead of root models for type definitions (e... |
| [`--use-unique-items-as-set`](#use-unique-items-as-set) | Generate set types for arrays with uniqueItems constraint. |

Expand Down Expand Up @@ -3084,6 +3085,59 @@ output types. Pydantic already uses these types by default.

---

## `--use-tuple-for-fixed-items` {#use-tuple-for-fixed-items}

Generate tuple types for arrays with items array syntax.

When `--use-tuple-for-fixed-items` is enabled and an array has `items` as an array
with `minItems == maxItems == len(items)`, generate a tuple type instead of a list.

!!! tip "Usage"

```bash
datamodel-codegen --input schema.json --use-tuple-for-fixed-items # (1)!
```

1. :material-arrow-left: `--use-tuple-for-fixed-items` - the option documented here

??? example "Examples"

**Input Schema:**

```json
{
"$schema": "https://json-schema.org/draft-07/schema",
"type": "object",
"properties": {
"point": {
"type": "array",
"items": [{"type": "number"}, {"type": "number"}],
"minItems": 2,
"maxItems": 2
}
},
"required": ["point"]
}
```

**Output:**

```python
# generated by datamodel-codegen:
# filename: items_array_tuple.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from pydantic import BaseModel


class Model(BaseModel):
point: tuple[float, float]
```

---

## `--use-type-alias` {#use-type-alias}

Use TypeAlias instead of root models for type definitions (experimental).
Expand Down
2 changes: 2 additions & 0 deletions src/datamodel_code_generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
use_title_as_name: bool = False,
use_operation_id_as_name: bool = False,
use_unique_items_as_set: bool = False,
use_tuple_for_fixed_items: bool = False,
allof_merge_mode: AllOfMergeMode = AllOfMergeMode.Constraints,
http_headers: Sequence[tuple[str, str]] | None = None,
http_ignore_tls: bool = False,
Expand Down Expand Up @@ -720,6 +721,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
use_title_as_name=use_title_as_name,
use_operation_id_as_name=use_operation_id_as_name,
use_unique_items_as_set=use_unique_items_as_set,
use_tuple_for_fixed_items=use_tuple_for_fixed_items,
allof_merge_mode=allof_merge_mode,
http_headers=http_headers,
http_ignore_tls=http_ignore_tls,
Expand Down
2 changes: 2 additions & 0 deletions src/datamodel_code_generator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ def validate_all_exports_collision_strategy(cls, values: dict[str, Any]) -> dict
use_title_as_name: bool = False
use_operation_id_as_name: bool = False
use_unique_items_as_set: bool = False
use_tuple_for_fixed_items: bool = False
allof_merge_mode: AllOfMergeMode = AllOfMergeMode.Constraints
http_headers: Optional[Sequence[tuple[str, str]]] = None # noqa: UP045
http_ignore_tls: bool = False
Expand Down Expand Up @@ -742,6 +743,7 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917
use_title_as_name=config.use_title_as_name,
use_operation_id_as_name=config.use_operation_id_as_name,
use_unique_items_as_set=config.use_unique_items_as_set,
use_tuple_for_fixed_items=config.use_tuple_for_fixed_items,
allof_merge_mode=config.allof_merge_mode,
http_headers=config.http_headers,
http_ignore_tls=config.http_ignore_tls,
Expand Down
6 changes: 6 additions & 0 deletions src/datamodel_code_generator/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,12 @@ def start_section(self, heading: str | None) -> None:
action="store_true",
default=None,
)
typing_options.add_argument(
"--use-tuple-for-fixed-items",
help="Generate tuple types for arrays with items array syntax when minItems equals maxItems equals items length",
action="store_true",
default=None,
)
typing_options.add_argument(
"--allof-merge-mode",
help="Mode for field merging in allOf schemas. "
Expand Down
1 change: 1 addition & 0 deletions src/datamodel_code_generator/cli_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ class CLIOptionMeta:
name="--use-non-positive-negative-number-constrained-types", category=OptionCategory.TYPING
),
"--use-unique-items-as-set": CLIOptionMeta(name="--use-unique-items-as-set", category=OptionCategory.TYPING),
"--use-tuple-for-fixed-items": CLIOptionMeta(name="--use-tuple-for-fixed-items", category=OptionCategory.TYPING),
"--type-mappings": CLIOptionMeta(name="--type-mappings", category=OptionCategory.TYPING),
"--no-use-specialized-enum": CLIOptionMeta(name="--no-use-specialized-enum", category=OptionCategory.TYPING),
"--allof-merge-mode": CLIOptionMeta(name="--allof-merge-mode", category=OptionCategory.TYPING),
Expand Down
2 changes: 2 additions & 0 deletions src/datamodel_code_generator/parser/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,7 @@ def __init__( # noqa: PLR0912, PLR0913, PLR0915
use_title_as_name: bool = False,
use_operation_id_as_name: bool = False,
use_unique_items_as_set: bool = False,
use_tuple_for_fixed_items: bool = False,
allof_merge_mode: AllOfMergeMode = AllOfMergeMode.Constraints,
http_headers: Sequence[tuple[str, str]] | None = None,
http_ignore_tls: bool = False,
Expand Down Expand Up @@ -842,6 +843,7 @@ def __init__( # noqa: PLR0912, PLR0913, PLR0915
self.use_title_as_name: bool = use_title_as_name
self.use_operation_id_as_name: bool = use_operation_id_as_name
self.use_unique_items_as_set: bool = use_unique_items_as_set
self.use_tuple_for_fixed_items: bool = use_tuple_for_fixed_items
self.allof_merge_mode: AllOfMergeMode = allof_merge_mode
self.dataclass_arguments = dataclass_arguments

Expand Down
2 changes: 2 additions & 0 deletions src/datamodel_code_generator/parser/graphql.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ def __init__( # noqa: PLR0913
use_title_as_name: bool = False,
use_operation_id_as_name: bool = False,
use_unique_items_as_set: bool = False,
use_tuple_for_fixed_items: bool = False,
allof_merge_mode: AllOfMergeMode = AllOfMergeMode.Constraints,
http_headers: Sequence[tuple[str, str]] | None = None,
http_ignore_tls: bool = False,
Expand Down Expand Up @@ -259,6 +260,7 @@ def __init__( # noqa: PLR0913
use_title_as_name=use_title_as_name,
use_operation_id_as_name=use_operation_id_as_name,
use_unique_items_as_set=use_unique_items_as_set,
use_tuple_for_fixed_items=use_tuple_for_fixed_items,
allof_merge_mode=allof_merge_mode,
http_headers=http_headers,
http_ignore_tls=http_ignore_tls,
Expand Down
27 changes: 15 additions & 12 deletions src/datamodel_code_generator/parser/jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@ def __init__( # noqa: PLR0913
use_title_as_name: bool = False,
use_operation_id_as_name: bool = False,
use_unique_items_as_set: bool = False,
use_tuple_for_fixed_items: bool = False,
allof_merge_mode: AllOfMergeMode = AllOfMergeMode.Constraints,
http_headers: Sequence[tuple[str, str]] | None = None,
http_ignore_tls: bool = False,
Expand Down Expand Up @@ -682,6 +683,7 @@ def __init__( # noqa: PLR0913
use_title_as_name=use_title_as_name,
use_operation_id_as_name=use_operation_id_as_name,
use_unique_items_as_set=use_unique_items_as_set,
use_tuple_for_fixed_items=use_tuple_for_fixed_items,
allof_merge_mode=allof_merge_mode,
http_headers=http_headers,
http_ignore_tls=http_ignore_tls,
Expand Down Expand Up @@ -895,6 +897,14 @@ def is_constraints_field(self, obj: JsonSchemaObject) -> bool:
)
)

def _is_fixed_length_tuple(self, obj: JsonSchemaObject) -> bool:
"""Check if an array field represents a fixed-length tuple."""
if obj.prefixItems is not None and obj.items in {None, False}:
return obj.minItems == obj.maxItems == len(obj.prefixItems)
if self.use_tuple_for_fixed_items and isinstance(obj.items, list) and obj.prefixItems is None:
return obj.minItems == obj.maxItems == len(obj.items)
return False

def _resolve_field_flag(self, obj: JsonSchemaObject, flag: Literal["readOnly", "writeOnly"]) -> bool:
"""Resolve a field flag (readOnly/writeOnly) from direct value, $ref, and compositions."""
if getattr(obj, flag) is True:
Expand Down Expand Up @@ -1046,12 +1056,7 @@ def get_object_field( # noqa: PLR0913
if constraints is not None and self.field_constraints and field.format == "hostname":
constraints["pattern"] = self.data_type_manager.HOSTNAME_REGEX
# Suppress minItems/maxItems for fixed-length tuples
if (
constraints
and field.prefixItems is not None
and field.minItems == field.maxItems == len(field.prefixItems)
and field.items in {None, False}
):
if constraints and self._is_fixed_length_tuple(field):
constraints.pop("minItems", None)
constraints.pop("maxItems", None)
return self.data_model_field_type(
Expand Down Expand Up @@ -2440,12 +2445,10 @@ def parse_array_fields( # noqa: PLR0912
items: list[JsonSchemaObject] = [obj.items]
elif isinstance(obj.items, list):
items = obj.items
elif (
obj.prefixItems is not None
and obj.minItems == obj.maxItems == len(obj.prefixItems)
and obj.items in {None, False}
):
# Suppress minItems/maxItems constraints for fixed-length tuples
if self._is_fixed_length_tuple(obj):
is_tuple = True
suppress_item_constraints = True
elif obj.prefixItems is not None and self._is_fixed_length_tuple(obj):
suppress_item_constraints = True
items = obj.prefixItems
is_tuple = True
Expand Down
2 changes: 2 additions & 0 deletions src/datamodel_code_generator/parser/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ def __init__( # noqa: PLR0913
use_title_as_name: bool = False,
use_operation_id_as_name: bool = False,
use_unique_items_as_set: bool = False,
use_tuple_for_fixed_items: bool = False,
allof_merge_mode: AllOfMergeMode = AllOfMergeMode.Constraints,
http_headers: Sequence[tuple[str, str]] | None = None,
http_ignore_tls: bool = False,
Expand Down Expand Up @@ -343,6 +344,7 @@ def __init__( # noqa: PLR0913
use_title_as_name=use_title_as_name,
use_operation_id_as_name=use_operation_id_as_name,
use_unique_items_as_set=use_unique_items_as_set,
use_tuple_for_fixed_items=use_tuple_for_fixed_items,
allof_merge_mode=allof_merge_mode,
http_headers=http_headers,
http_ignore_tls=http_ignore_tls,
Expand Down
11 changes: 11 additions & 0 deletions tests/data/expected/main/jsonschema/items_array_tuple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# generated by datamodel-codegen:
# filename: items_array_tuple.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from pydantic import BaseModel


class Model(BaseModel):
point: tuple[float, float]
13 changes: 13 additions & 0 deletions tests/data/jsonschema/items_array_tuple.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"type": "object",
"properties": {
"point": {
"type": "array",
"items": [{"type": "number"}, {"type": "number"}],
"minItems": 2,
"maxItems": 2
}
},
"required": ["point"]
}
33 changes: 33 additions & 0 deletions tests/main/jsonschema/test_main_jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3363,6 +3363,39 @@ def test_main_jsonschema_prefix_items_no_tuple(min_version: str, output_file: Pa
)


@freeze_time("2019-07-26")
@pytest.mark.skipif(
int(black.__version__.split(".")[0]) < 24,
reason="Installed black doesn't support the new style",
)
@pytest.mark.cli_doc(
options=["--use-tuple-for-fixed-items"],
input_schema="jsonschema/items_array_tuple.json",
cli_args=["--use-tuple-for-fixed-items"],
golden_output="jsonschema/items_array_tuple.py",
)
def test_main_jsonschema_items_array_tuple(min_version: str, output_file: Path) -> None:
"""Generate tuple types for arrays with items array syntax.

When `--use-tuple-for-fixed-items` is enabled and an array has `items` as an array
with `minItems == maxItems == len(items)`, generate a tuple type instead of a list.
"""
run_main_and_assert(
input_path=JSON_SCHEMA_DATA_PATH / "items_array_tuple.json",
output_path=output_file,
input_file_type=None,
assert_func=assert_file_content,
expected_file="items_array_tuple.py",
extra_args=[
"--output-model-type",
"pydantic_v2.BaseModel",
"--target-python-version",
min_version,
"--use-tuple-for-fixed-items",
],
)


@pytest.mark.skipif(
int(black.__version__.split(".")[0]) < 24,
reason="Installed black doesn't support the new style",
Expand Down
Loading