Skip to content
Merged
66 changes: 66 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) | Configure how to resolve naming conflicts between field name... |
| [`--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,71 @@ The `--field-include-all-keys` flag configures the code generation behavior.

---

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

Configure how to resolve naming conflicts between field names and generated type names.

When a schema property name matches a generated type name (e.g., a property "Fruit" with
an enum type that would also be named "Fruit"), a collision occurs. This option controls
the resolution strategy:

- `rename-field` (default): Rename the field with a suffix (e.g., `Fruit_1`) and preserve
the original name via `Field(alias='Fruit')`
- `rename-type`: Rename the type class with a suffix (e.g., `Fruit_`) and keep the original
field name

!!! 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": {
"Fruit": {
"enum": [
"apple",
"banana"
]
}
}
}
```

**Output:**

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

from __future__ import annotations

from enum import Enum

from pydantic import BaseModel


class Fruit_1(Enum):
apple = 'apple'
banana = 'banana'


class Test(BaseModel):
Fruit: Fruit_1 | None = None
```

---
Comment thread
koxudaxi marked this conversation as resolved.

## `--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) | Configure how to resolve naming conflicts between field names and generated type... |
| [`--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) - Configure how to resolve naming conflicts between field name...
- [`--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
14 changes: 14 additions & 0 deletions src/datamodel_code_generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,17 @@ class ModuleSplitMode(Enum):
Single = "single"


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

RenameField: Rename the field with a suffix (e.g., Fruit_1) and add alias (default).
RenameType: Rename the type class with a suffix (e.g., Fruit_) to preserve field name.
"""

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


class Error(Exception):
"""Base exception for datamodel-code-generator errors."""

Expand Down Expand Up @@ -485,6 +496,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
all_exports_scope: AllExportsScope | None = None,
all_exports_collision_strategy: AllExportsCollisionStrategy | None = None,
module_split_mode: ModuleSplitMode | None = None,
field_type_collision_strategy: FieldTypeCollisionStrategy | 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 Expand Up @@ -876,6 +889,7 @@ def infer_input_type(text: str) -> InputFileType:
"DatetimeClassType",
"DefaultPutDict",
"Error",
"FieldTypeCollisionStrategy",
"InputFileType",
"InvalidClassNameError",
"LiteralType",
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 @@ -471,6 +472,7 @@ def validate_all_exports_collision_strategy(cls, values: dict[str, Any]) -> dict
all_exports_scope: Optional[AllExportsScope] = None # noqa: UP045
all_exports_collision_strategy: Optional[AllExportsCollisionStrategy] = None # noqa: UP045
module_split_mode: Optional[ModuleSplitMode] = None # noqa: UP045
field_type_collision_strategy: Optional[FieldTypeCollisionStrategy] = None # noqa: UP045
watch: bool = False
watch_delay: float = 0.5

Expand Down Expand Up @@ -778,6 +780,7 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917
all_exports_scope=config.all_exports_scope,
all_exports_collision_strategy=config.all_exports_collision_strategy,
module_split_mode=config.module_split_mode,
field_type_collision_strategy=config.field_type_collision_strategy,
)


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 @@ -601,6 +602,14 @@ def start_section(self, heading: str | None) -> None:
choices=[u.value for u in UnionMode],
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,
)
field_options.add_argument(
"--no-alias",
help="""Do not add a field alias. E.g., if --snake-case-field is used along with a base class, which has an
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
35 changes: 25 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,34 @@ def __change_field_name(
) -> None:
if not self.data_model_type.SUPPORTS_FIELD_RENAMING:
return

rename_type = self.field_type_collision_strategy == FieldTypeCollisionStrategy.RenameType

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)
resolver = ModelResolver(snake_case_field=self.snake_case_field, remove_suffix_number=True)
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
resolver.exclude_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:
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
else:
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,18 @@
# generated by datamodel-codegen:
# filename: field_type_collision_rename_type.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from enum import Enum

from pydantic import BaseModel


class Fruit_1(Enum):
apple = 'apple'
banana = 'banana'


class Test(BaseModel):
Fruit: Fruit_1 | None = None
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# generated by datamodel-codegen:
# filename: field_type_collision_rename_type_multi_model.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from enum import Enum

from pydantic import BaseModel


class Fruit_1(Enum):
apple = 'apple'
banana = 'banana'


class Child(BaseModel):
Fruit: Fruit_1 | None = None


class Parent(BaseModel):
Fruit: Fruit_1 | None = None
child: Child | None = None
Loading
Loading