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
61 changes: 61 additions & 0 deletions docs/cli-reference/field-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
| [`--field-extra-keys`](#field-extra-keys) | Include specific extra keys in Field() definitions. |
| [`--field-extra-keys-without-x-prefix`](#field-extra-keys-without-x-prefix) | Include specified schema extension keys in Field() without r... |
| [`--field-include-all-keys`](#field-include-all-keys) | Include all schema keys in Field() json_schema_extra. |
| [`--field-type-collision-strategy`](#field-type-collision-strategy) | Rename type class instead of field when names collide (Pydan... |
| [`--no-alias`](#no-alias) | Disable Field alias generation for non-Python-safe property ... |
| [`--original-field-name-delimiter`](#original-field-name-delimiter) | Specify delimiter for original field names when using snake-... |
| [`--remove-special-field-name-prefix`](#remove-special-field-name-prefix) | Remove the special prefix from field names. |
Expand Down Expand Up @@ -1813,6 +1814,66 @@ The `--field-include-all-keys` flag configures the code generation behavior.

---

## `--field-type-collision-strategy` {#field-type-collision-strategy}

Rename type class instead of field when names collide (Pydantic v2 only).

The `--field-type-collision-strategy` flag controls how field name and type name
collisions are resolved. With `rename-type`, the type class is renamed with a suffix
to preserve the original field name, instead of renaming the field and adding an alias.

!!! tip "Usage"

```bash
datamodel-codegen --input schema.json --output-model-type pydantic_v2.BaseModel --field-type-collision-strategy rename-type # (1)!
```

1. :material-arrow-left: `--field-type-collision-strategy` - the option documented here

??? example "Examples"

**Input Schema:**

```json
{
"title": "Test",
"type": "object",
"properties": {
"TestObject": {
"title": "TestObject",
"type": "object",
"properties": {
"test_string": {
"type": "string"
}
}
}
}
}
```

**Output:**

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

from __future__ import annotations

from pydantic import BaseModel, Field


class TestObject_1(BaseModel):
test_string: str | None = None


class Test(BaseModel):
TestObject: TestObject_1 | None = Field(None, title='TestObject')
```

---

## `--no-alias` {#no-alias}

Disable Field alias generation for non-Python-safe property names.
Expand Down
3 changes: 2 additions & 1 deletion docs/cli-reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This documentation is auto-generated from test cases.
|----------|---------|-------------|
| 📁 [Base Options](base-options.md) | 5 | Input/output configuration |
| 🔧 [Typing Customization](typing-customization.md) | 17 | Type annotation and import behavior |
| 🏷️ [Field Customization](field-customization.md) | 20 | Field naming and docstring behavior |
| 🏷️ [Field Customization](field-customization.md) | 21 | Field naming and docstring behavior |
| 🏗️ [Model Customization](model-customization.md) | 29 | Model generation behavior |
| 🎨 [Template Customization](template-customization.md) | 16 | Output formatting and custom rendering |
| 📘 [OpenAPI-only Options](openapi-only-options.md) | 6 | OpenAPI-specific features |
Expand Down Expand Up @@ -75,6 +75,7 @@ This documentation is auto-generated from test cases.
- [`--field-extra-keys`](field-customization.md#field-extra-keys)
- [`--field-extra-keys-without-x-prefix`](field-customization.md#field-extra-keys-without-x-prefix)
- [`--field-include-all-keys`](field-customization.md#field-include-all-keys)
- [`--field-type-collision-strategy`](field-customization.md#field-type-collision-strategy)
- [`--force-optional`](model-customization.md#force-optional)
- [`--formatters`](template-customization.md#formatters)
- [`--frozen-dataclasses`](model-customization.md#frozen-dataclasses)
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 @@ -56,6 +56,7 @@ datamodel-codegen [OPTIONS]
| [`--field-extra-keys`](field-customization.md#field-extra-keys) | Include specific extra keys in Field() definitions. |
| [`--field-extra-keys-without-x-prefix`](field-customization.md#field-extra-keys-without-x-prefix) | Include specified schema extension keys in Field() without requiring 'x-' prefix... |
| [`--field-include-all-keys`](field-customization.md#field-include-all-keys) | Include all schema keys in Field() json_schema_extra. |
| [`--field-type-collision-strategy`](field-customization.md#field-type-collision-strategy) | Rename type class instead of field when names collide (Pydantic v2 only). |
| [`--no-alias`](field-customization.md#no-alias) | Disable Field alias generation for non-Python-safe property names. |
| [`--original-field-name-delimiter`](field-customization.md#original-field-name-delimiter) | Specify delimiter for original field names when using snake-case conversion. |
| [`--remove-special-field-name-prefix`](field-customization.md#remove-special-field-name-prefix) | Remove the special prefix from field names. |
Expand Down Expand Up @@ -206,6 +207,7 @@ All options sorted alphabetically:
- [`--field-extra-keys`](field-customization.md#field-extra-keys) - Include specific extra keys in Field() definitions.
- [`--field-extra-keys-without-x-prefix`](field-customization.md#field-extra-keys-without-x-prefix) - Include specified schema extension keys in Field() without r...
- [`--field-include-all-keys`](field-customization.md#field-include-all-keys) - Include all schema keys in Field() json_schema_extra.
- [`--field-type-collision-strategy`](field-customization.md#field-type-collision-strategy) - Rename type class instead of field when names collide (Pydan...
- [`--force-optional`](model-customization.md#force-optional) - Force all fields to be Optional regardless of required statu...
- [`--formatters`](template-customization.md#formatters) - Specify code formatters to apply to generated output.
- [`--frozen-dataclasses`](model-customization.md#frozen-dataclasses) - Generate frozen dataclasses with optional keyword-only field...
Expand Down
13 changes: 13 additions & 0 deletions src/datamodel_code_generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,17 @@ class AllExportsCollisionStrategy(Enum):
FullPrefix = "full-prefix"


class FieldTypeCollisionStrategy(Enum):
"""Strategy for handling field name and type name collisions.

rename_field: Rename the field with a suffix and add alias (default).
rename_type: Rename the type class with a suffix to preserve field name.
"""

RenameField = "rename-field"
RenameType = "rename-type"


class AllOfMergeMode(Enum):
"""Mode for field merging in allOf schemas.

Expand Down Expand Up @@ -484,6 +495,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
use_status_code_in_response_name: bool = False,
all_exports_scope: AllExportsScope | None = None,
all_exports_collision_strategy: AllExportsCollisionStrategy | None = None,
field_type_collision_strategy: FieldTypeCollisionStrategy | None = None,
module_split_mode: ModuleSplitMode | None = None,
) -> None:
"""Generate Python data models from schema definitions or structured data.
Expand Down Expand Up @@ -730,6 +742,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
dataclass_arguments=dataclass_arguments,
type_mappings=type_mappings,
read_only_write_only_model_type=read_only_write_only_model_type,
field_type_collision_strategy=field_type_collision_strategy,
**kwargs,
)

Expand Down
3 changes: 3 additions & 0 deletions src/datamodel_code_generator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
DataclassArguments,
DataModelType,
Error,
FieldTypeCollisionStrategy,
InputFileType,
InvalidClassNameError,
ModuleSplitMode,
Expand Down Expand Up @@ -470,6 +471,7 @@ def validate_all_exports_collision_strategy(cls, values: dict[str, Any]) -> dict
use_status_code_in_response_name: bool = False
all_exports_scope: Optional[AllExportsScope] = None # noqa: UP045
all_exports_collision_strategy: Optional[AllExportsCollisionStrategy] = None # noqa: UP045
field_type_collision_strategy: Optional[FieldTypeCollisionStrategy] = None # noqa: UP045
module_split_mode: Optional[ModuleSplitMode] = None # noqa: UP045
watch: bool = False
watch_delay: float = 0.5
Expand Down Expand Up @@ -777,6 +779,7 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917
use_status_code_in_response_name=config.use_status_code_in_response_name,
all_exports_scope=config.all_exports_scope,
all_exports_collision_strategy=config.all_exports_collision_strategy,
field_type_collision_strategy=config.field_type_collision_strategy,
module_split_mode=config.module_split_mode,
)

Expand Down
9 changes: 9 additions & 0 deletions src/datamodel_code_generator/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
AllOfMergeMode,
DataclassArguments,
DataModelType,
FieldTypeCollisionStrategy,
InputFileType,
ModuleSplitMode,
OpenAPIScope,
Expand Down Expand Up @@ -621,6 +622,14 @@ def start_section(self, heading: str | None) -> None:
action="store_true",
default=None,
)
field_options.add_argument(
"--field-type-collision-strategy",
help="Strategy for handling field name and type name collisions (Pydantic v2 only). "
"'rename-field': rename field with suffix and add alias (default). "
"'rename-type': rename type class with suffix to preserve field name.",
choices=[s.value for s in FieldTypeCollisionStrategy],
default=None,
)

# ======================================================================================
# Options for templating output
Expand Down
3 changes: 3 additions & 0 deletions src/datamodel_code_generator/cli_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ class CLIOptionMeta:
"--use-enum-values-in-discriminator": CLIOptionMeta(
name="--use-enum-values-in-discriminator", category=OptionCategory.FIELD
),
"--field-type-collision-strategy": CLIOptionMeta(
name="--field-type-collision-strategy", category=OptionCategory.FIELD
),
# ==========================================================================
# Typing Customization
# ==========================================================================
Expand Down
48 changes: 38 additions & 10 deletions src/datamodel_code_generator/parser/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
AllExportsScope,
AllOfMergeMode,
Error,
FieldTypeCollisionStrategy,
ModuleSplitMode,
ReadOnlyWriteOnlyModelType,
ReuseScope,
Expand Down Expand Up @@ -770,6 +771,7 @@ def __init__( # noqa: PLR0912, PLR0913, PLR0915
dataclass_arguments: DataclassArguments | None = None,
type_mappings: list[str] | None = None,
read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = None,
field_type_collision_strategy: FieldTypeCollisionStrategy | None = None,
) -> None:
"""Initialize the Parser with configuration options."""
self.keyword_only = keyword_only
Expand Down Expand Up @@ -926,6 +928,7 @@ def __init__( # noqa: PLR0912, PLR0913, PLR0915
self.read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = read_only_write_only_model_type
self.use_frozen_field: bool = use_frozen_field
self.use_default_factory_for_optional_nested_models: bool = use_default_factory_for_optional_nested_models
self.field_type_collision_strategy: FieldTypeCollisionStrategy | None = field_type_collision_strategy

@property
def field_name_model_type(self) -> ModelType:
Expand Down Expand Up @@ -1844,22 +1847,47 @@ def __change_field_name(
) -> None:
if not self.data_model_type.SUPPORTS_FIELD_RENAMING:
return

rename_type = self.field_type_collision_strategy == FieldTypeCollisionStrategy.RenameType
all_class_names = {cast("str", m.class_name) for m in models if m.class_name}

for model in models:
if "Enum" in model.base_class:
continue
if not model.BASE_CLASS:
if "Enum" in model.base_class or not model.BASE_CLASS:
continue

for field in model.fields:
filed_name = field.name
filed_name_resolver = ModelResolver(snake_case_field=self.snake_case_field, remove_suffix_number=True)
reference_type_names: set[str] = set()
colliding_reference: Reference | None = None

for data_type in field.data_type.all_data_types:
if data_type.reference:
filed_name_resolver.exclude_names.add(data_type.reference.short_name)
new_filed_name = filed_name_resolver.add(["field"], cast("str", filed_name)).name
if filed_name != new_filed_name:
field.alias = filed_name
field.name = new_filed_name
if not data_type.reference:
continue
reference_type_names.add(data_type.reference.short_name)
if rename_type and colliding_reference is None and data_type.reference.short_name == filed_name:
colliding_reference = data_type.reference

if colliding_reference is not None:
resolver = ModelResolver(
exclude_names=all_class_names.copy(),
snake_case_field=self.snake_case_field,
remove_suffix_number=True,
)
source = cast("DataModel", colliding_reference.source)
resolver.exclude_names.add(cast("str", filed_name))
new_class_name = resolver.add(["type"], cast("str", source.class_name)).name
source.class_name = new_class_name
all_class_names.add(new_class_name)
elif not rename_type:
resolver = ModelResolver(
exclude_names=reference_type_names,
snake_case_field=self.snake_case_field,
remove_suffix_number=True,
)
new_filed_name = resolver.add(["field"], cast("str", filed_name)).name
if filed_name != new_filed_name:
field.alias = filed_name
field.name = new_filed_name

def __set_one_literal_on_default(self, models: list[DataModel]) -> None:
if not self.use_one_literal_as_default:
Expand Down
3 changes: 3 additions & 0 deletions src/datamodel_code_generator/parser/graphql.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
AllOfMergeMode,
DataclassArguments,
DefaultPutDict,
FieldTypeCollisionStrategy,
LiteralType,
PythonVersion,
PythonVersionMin,
Expand Down Expand Up @@ -193,6 +194,7 @@ def __init__( # noqa: PLR0913
use_serialize_as_any: bool = False,
use_frozen_field: bool = False,
use_default_factory_for_optional_nested_models: bool = False,
field_type_collision_strategy: FieldTypeCollisionStrategy | None = None,
) -> None:
"""Initialize the GraphQL parser with configuration options."""
super().__init__(
Expand Down Expand Up @@ -290,6 +292,7 @@ def __init__( # noqa: PLR0913
use_serialize_as_any=use_serialize_as_any,
use_frozen_field=use_frozen_field,
use_default_factory_for_optional_nested_models=use_default_factory_for_optional_nested_models,
field_type_collision_strategy=field_type_collision_strategy,
)

self.data_model_scalar_type = data_model_scalar_type
Expand Down
3 changes: 3 additions & 0 deletions src/datamodel_code_generator/parser/jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
DEFAULT_SHARED_MODULE_NAME,
AllOfMergeMode,
DataclassArguments,
FieldTypeCollisionStrategy,
InvalidClassNameError,
ReadOnlyWriteOnlyModelType,
ReuseScope,
Expand Down Expand Up @@ -605,6 +606,7 @@ def __init__( # noqa: PLR0913
dataclass_arguments: DataclassArguments | None = None,
type_mappings: list[str] | None = None,
read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = None,
field_type_collision_strategy: FieldTypeCollisionStrategy | None = None,
) -> None:
"""Initialize the JSON Schema parser with configuration options."""
target_datetime_class = target_datetime_class or DatetimeClassType.Awaredatetime
Expand Down Expand Up @@ -703,6 +705,7 @@ def __init__( # noqa: PLR0913
dataclass_arguments=dataclass_arguments,
type_mappings=type_mappings,
read_only_write_only_model_type=read_only_write_only_model_type,
field_type_collision_strategy=field_type_collision_strategy,
)

self.remote_object_cache: DefaultPutDict[str, dict[str, YamlValue]] = DefaultPutDict()
Expand Down
3 changes: 3 additions & 0 deletions src/datamodel_code_generator/parser/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
AllOfMergeMode,
DataclassArguments,
Error,
FieldTypeCollisionStrategy,
LiteralType,
OpenAPIScope,
PythonVersion,
Expand Down Expand Up @@ -277,6 +278,7 @@ def __init__( # noqa: PLR0913
use_frozen_field: bool = False,
use_default_factory_for_optional_nested_models: bool = False,
use_status_code_in_response_name: bool = False,
field_type_collision_strategy: FieldTypeCollisionStrategy | None = None,
) -> None:
"""Initialize the OpenAPI parser with extensive configuration options."""
target_datetime_class = target_datetime_class or DatetimeClassType.Awaredatetime
Expand Down Expand Up @@ -375,6 +377,7 @@ def __init__( # noqa: PLR0913
read_only_write_only_model_type=read_only_write_only_model_type,
use_frozen_field=use_frozen_field,
use_default_factory_for_optional_nested_models=use_default_factory_for_optional_nested_models,
field_type_collision_strategy=field_type_collision_strategy,
)
self.open_api_scopes: list[OpenAPIScope] = openapi_scopes or [OpenAPIScope.Schemas]
self.include_path_parameters: bool = include_path_parameters
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# generated by datamodel-codegen:
# filename: field_has_same_name.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from pydantic import BaseModel, Field


class TestObject_1(BaseModel):
test_string: str | None = None


class Test(BaseModel):
TestObject: TestObject_1 | None = Field(None, title='TestObject')
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# generated by datamodel-codegen:
# filename: field_type_collision_rename_type_double.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from enum import Enum

from pydantic import BaseModel


class Status_1(Enum):
pending = 'pending'
confirmed = 'confirmed'


class Status1(BaseModel):
code: int | None = None


class Order(BaseModel):
Status: Status_1 | None = None
Loading
Loading