From b8d546cd9a131cc37400878580db1d8ad3cc2db5 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 22 Dec 2025 09:23:34 +0000 Subject: [PATCH 1/8] Add --field-type-collision-strategy option --- src/datamodel_code_generator/__init__.py | 14 +++++++ src/datamodel_code_generator/__main__.py | 3 ++ src/datamodel_code_generator/arguments.py | 9 +++++ src/datamodel_code_generator/cli_options.py | 3 ++ src/datamodel_code_generator/parser/base.py | 33 ++++++++++----- .../parser/graphql.py | 3 ++ .../parser/jsonschema.py | 3 ++ .../parser/openapi.py | 3 ++ .../field_type_collision_rename_type.py | 18 +++++++++ ..._type_collision_rename_type_multi_model.py | 23 +++++++++++ .../field_type_collision_rename_type.json | 12 ++++++ ...ype_collision_rename_type_multi_model.json | 27 +++++++++++++ tests/main/jsonschema/test_main_jsonschema.py | 40 +++++++++++++++++++ 13 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 tests/data/expected/main/jsonschema/field_type_collision_rename_type.py create mode 100644 tests/data/expected/main/jsonschema/field_type_collision_rename_type_multi_model.py create mode 100644 tests/data/jsonschema/field_type_collision_rename_type.json create mode 100644 tests/data/jsonschema/field_type_collision_rename_type_multi_model.json diff --git a/src/datamodel_code_generator/__init__.py b/src/datamodel_code_generator/__init__.py index 823af002e..b61491a28 100644 --- a/src/datamodel_code_generator/__init__.py +++ b/src/datamodel_code_generator/__init__.py @@ -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.""" @@ -484,6 +495,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. @@ -728,6 +740,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, ) @@ -874,6 +887,7 @@ def infer_input_type(text: str) -> InputFileType: "DatetimeClassType", "DefaultPutDict", "Error", + "FieldTypeCollisionStrategy", "InputFileType", "InvalidClassNameError", "LiteralType", diff --git a/src/datamodel_code_generator/__main__.py b/src/datamodel_code_generator/__main__.py index f573d3b1e..0eb307511 100644 --- a/src/datamodel_code_generator/__main__.py +++ b/src/datamodel_code_generator/__main__.py @@ -28,6 +28,7 @@ DataclassArguments, DataModelType, Error, + FieldTypeCollisionStrategy, InputFileType, InvalidClassNameError, ModuleSplitMode, @@ -470,6 +471,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 @@ -776,6 +778,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, ) diff --git a/src/datamodel_code_generator/arguments.py b/src/datamodel_code_generator/arguments.py index 1c7c11078..70b919ee9 100644 --- a/src/datamodel_code_generator/arguments.py +++ b/src/datamodel_code_generator/arguments.py @@ -21,6 +21,7 @@ AllOfMergeMode, DataclassArguments, DataModelType, + FieldTypeCollisionStrategy, InputFileType, ModuleSplitMode, OpenAPIScope, @@ -594,6 +595,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 diff --git a/src/datamodel_code_generator/cli_options.py b/src/datamodel_code_generator/cli_options.py index 0f1ab4a9c..16338806c 100644 --- a/src/datamodel_code_generator/cli_options.py +++ b/src/datamodel_code_generator/cli_options.py @@ -134,6 +134,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 # ========================================================================== diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index f6963e359..e8b378a1c 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -28,6 +28,7 @@ AllExportsScope, AllOfMergeMode, Error, + FieldTypeCollisionStrategy, ModuleSplitMode, ReadOnlyWriteOnlyModelType, ReuseScope, @@ -769,6 +770,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 @@ -924,6 +926,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: @@ -1842,22 +1845,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) + colliding_type: DataType | 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 + filed_name_resolver.exclude_names.add(data_type.reference.short_name) + if rename_type and colliding_type is None and data_type.reference.short_name == filed_name: + colliding_type = data_type + + if colliding_type is not None: + source = colliding_type.reference.source # type: ignore[union-attr] + if isinstance(source, DataModel): + source.class_name = f"{source.class_name}_" + else: + 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 def __set_one_literal_on_default(self, models: list[DataModel]) -> None: if not self.use_one_literal_as_default: diff --git a/src/datamodel_code_generator/parser/graphql.py b/src/datamodel_code_generator/parser/graphql.py index da76f4159..222fa38fd 100644 --- a/src/datamodel_code_generator/parser/graphql.py +++ b/src/datamodel_code_generator/parser/graphql.py @@ -18,6 +18,7 @@ AllOfMergeMode, DataclassArguments, DefaultPutDict, + FieldTypeCollisionStrategy, LiteralType, PythonVersion, PythonVersionMin, @@ -192,6 +193,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__( @@ -288,6 +290,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 diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index fd1cca6dc..7d7b7b845 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -25,6 +25,7 @@ DEFAULT_SHARED_MODULE_NAME, AllOfMergeMode, DataclassArguments, + FieldTypeCollisionStrategy, InvalidClassNameError, ReadOnlyWriteOnlyModelType, ReuseScope, @@ -604,6 +605,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 @@ -701,6 +703,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() diff --git a/src/datamodel_code_generator/parser/openapi.py b/src/datamodel_code_generator/parser/openapi.py index f18523d6d..ed3127f80 100644 --- a/src/datamodel_code_generator/parser/openapi.py +++ b/src/datamodel_code_generator/parser/openapi.py @@ -22,6 +22,7 @@ AllOfMergeMode, DataclassArguments, Error, + FieldTypeCollisionStrategy, LiteralType, OpenAPIScope, PythonVersion, @@ -276,6 +277,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 @@ -373,6 +375,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 diff --git a/tests/data/expected/main/jsonschema/field_type_collision_rename_type.py b/tests/data/expected/main/jsonschema/field_type_collision_rename_type.py new file mode 100644 index 000000000..056f8c829 --- /dev/null +++ b/tests/data/expected/main/jsonschema/field_type_collision_rename_type.py @@ -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_(Enum): + apple = 'apple' + banana = 'banana' + + +class Test(BaseModel): + Fruit: Fruit_ | None = None diff --git a/tests/data/expected/main/jsonschema/field_type_collision_rename_type_multi_model.py b/tests/data/expected/main/jsonschema/field_type_collision_rename_type_multi_model.py new file mode 100644 index 000000000..84a282470 --- /dev/null +++ b/tests/data/expected/main/jsonschema/field_type_collision_rename_type_multi_model.py @@ -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_(Enum): + apple = 'apple' + banana = 'banana' + + +class Child(BaseModel): + Fruit: Fruit_ | None = None + + +class Parent(BaseModel): + Fruit: Fruit_ | None = None + child: Child | None = None diff --git a/tests/data/jsonschema/field_type_collision_rename_type.json b/tests/data/jsonschema/field_type_collision_rename_type.json new file mode 100644 index 000000000..547d2166e --- /dev/null +++ b/tests/data/jsonschema/field_type_collision_rename_type.json @@ -0,0 +1,12 @@ +{ + "title": "Test", + "type": "object", + "properties": { + "Fruit": { + "enum": [ + "apple", + "banana" + ] + } + } +} diff --git a/tests/data/jsonschema/field_type_collision_rename_type_multi_model.json b/tests/data/jsonschema/field_type_collision_rename_type_multi_model.json new file mode 100644 index 000000000..8fdfdc83c --- /dev/null +++ b/tests/data/jsonschema/field_type_collision_rename_type_multi_model.json @@ -0,0 +1,27 @@ +{ + "title": "Parent", + "type": "object", + "properties": { + "Fruit": { + "$ref": "#/definitions/Fruit" + }, + "child": { + "$ref": "#/definitions/Child" + } + }, + "definitions": { + "Fruit": { + "title": "Fruit", + "enum": ["apple", "banana"] + }, + "Child": { + "title": "Child", + "type": "object", + "properties": { + "Fruit": { + "$ref": "#/definitions/Fruit" + } + } + } + } +} diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index 1475e83a3..c1cedb610 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -3572,6 +3572,46 @@ def test_main_jsonschema_field_has_same_name(output_model: str, expected_output: ) +@pytest.mark.cli_doc( + options=["--field-type-collision-strategy"], + input_schema="jsonschema/field_type_collision_rename_type.json", + cli_args=["--output-model-type", "pydantic_v2.BaseModel", "--field-type-collision-strategy", "rename-type"], + golden_output="field_type_collision_rename_type.py", +) +def test_main_jsonschema_field_type_collision_rename_type(output_file: Path) -> None: + """Test field-type collision with rename-type strategy.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "field_type_collision_rename_type.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="field_type_collision_rename_type.py", + extra_args=[ + "--output-model-type", + "pydantic_v2.BaseModel", + "--field-type-collision-strategy", + "rename-type", + ], + ) + + +def test_main_jsonschema_field_type_collision_rename_type_multi_model(output_file: Path) -> None: + """Test field-type collision with rename-type strategy across multiple models.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "field_type_collision_rename_type_multi_model.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="field_type_collision_rename_type_multi_model.py", + extra_args=[ + "--output-model-type", + "pydantic_v2.BaseModel", + "--field-type-collision-strategy", + "rename-type", + ], + ) + + @pytest.mark.benchmark def test_main_jsonschema_required_and_any_of_required(output_file: Path) -> None: """Test required field with anyOf required.""" From 14a4bef7764ef5ccf338feba28f22bc6763ab08c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 22 Dec 2025 09:24:11 +0000 Subject: [PATCH 2/8] docs: update CLI reference documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by GitHub Actions --- docs/cli-reference/field-customization.md | 38 +++++++++++++++++++++++ docs/cli-reference/index.md | 3 +- docs/cli-reference/quick-reference.md | 2 ++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/cli-reference/field-customization.md b/docs/cli-reference/field-customization.md index 8156d06c3..797671cfd 100644 --- a/docs/cli-reference/field-customization.md +++ b/docs/cli-reference/field-customization.md @@ -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) | Test field-type collision with rename-type strategy. | | [`--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. | @@ -1813,6 +1814,43 @@ The `--field-include-all-keys` flag configures the code generation behavior. --- +## `--field-type-collision-strategy` {#field-type-collision-strategy} + +Test field-type collision with rename-type strategy. + +!!! 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:** + + > **Error:** File not found: field_type_collision_rename_type.py + +--- + ## `--no-alias` {#no-alias} Disable Field alias generation for non-Python-safe property names. diff --git a/docs/cli-reference/index.md b/docs/cli-reference/index.md index 083dbe03d..2f3a6e633 100644 --- a/docs/cli-reference/index.md +++ b/docs/cli-reference/index.md @@ -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) | 28 | 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 | @@ -74,6 +74,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) diff --git a/docs/cli-reference/quick-reference.md b/docs/cli-reference/quick-reference.md index b255c85d5..1a9d38d47 100644 --- a/docs/cli-reference/quick-reference.md +++ b/docs/cli-reference/quick-reference.md @@ -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) | Test field-type collision with rename-type strategy. | | [`--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. | @@ -204,6 +205,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) - Test field-type collision with rename-type strategy. - [`--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... From a223a37d526c85dad4f6962b5a168886ab426d91 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 22 Dec 2025 10:30:05 +0000 Subject: [PATCH 3/8] Remove untestable isinstance branch by using cast --- src/datamodel_code_generator/parser/base.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index e8b378a1c..9581f1908 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -1855,19 +1855,20 @@ def __change_field_name( for field in model.fields: filed_name = field.name filed_name_resolver = ModelResolver(snake_case_field=self.snake_case_field, remove_suffix_number=True) - colliding_type: DataType | None = None - + colliding_reference: Reference | None = None for data_type in field.data_type.all_data_types: if not data_type.reference: continue filed_name_resolver.exclude_names.add(data_type.reference.short_name) - if rename_type and colliding_type is None and data_type.reference.short_name == filed_name: - colliding_type = data_type - - if colliding_type is not None: - source = colliding_type.reference.source # type: ignore[union-attr] - if isinstance(source, DataModel): - source.class_name = f"{source.class_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: + # When a field name collides with a type name, the type's reference source + # is always a DataModel (Enum, model class, etc.) because DataModel.__init__ + # sets reference.source = self + source = cast("DataModel", colliding_reference.source) + source.class_name = f"{source.class_name}_" else: new_filed_name = filed_name_resolver.add(["field"], cast("str", filed_name)).name if filed_name != new_filed_name: From b53934e94bf10b423889eda3ba096b5c34175b61 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 22 Dec 2025 11:19:24 +0000 Subject: [PATCH 4/8] Improve CLI documentation for --field-type-collision-strategy --- tests/main/jsonschema/test_main_jsonschema.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index c1cedb610..4a60a1477 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -3576,10 +3576,20 @@ def test_main_jsonschema_field_has_same_name(output_model: str, expected_output: options=["--field-type-collision-strategy"], input_schema="jsonschema/field_type_collision_rename_type.json", cli_args=["--output-model-type", "pydantic_v2.BaseModel", "--field-type-collision-strategy", "rename-type"], - golden_output="field_type_collision_rename_type.py", + golden_output="main/jsonschema/field_type_collision_rename_type.py", ) def test_main_jsonschema_field_type_collision_rename_type(output_file: Path) -> None: - """Test field-type collision with rename-type 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 + """ run_main_and_assert( input_path=JSON_SCHEMA_DATA_PATH / "field_type_collision_rename_type.json", output_path=output_file, From d67b0a0e571084131e218faac12db6b58c9f18c6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 22 Dec 2025 11:19:52 +0000 Subject: [PATCH 5/8] docs: update CLI reference documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by GitHub Actions --- docs/cli-reference/field-customization.md | 34 +++++++++++++++++++++-- docs/cli-reference/quick-reference.md | 4 +-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/docs/cli-reference/field-customization.md b/docs/cli-reference/field-customization.md index 797671cfd..7c15c3aab 100644 --- a/docs/cli-reference/field-customization.md +++ b/docs/cli-reference/field-customization.md @@ -12,7 +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) | Test field-type collision with rename-type strategy. | +| [`--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. | @@ -1816,7 +1816,16 @@ The `--field-include-all-keys` flag configures the code generation behavior. ## `--field-type-collision-strategy` {#field-type-collision-strategy} -Test field-type collision with rename-type 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" @@ -1847,7 +1856,26 @@ Test field-type collision with rename-type strategy. **Output:** - > **Error:** File not found: field_type_collision_rename_type.py + ```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_(Enum): + apple = 'apple' + banana = 'banana' + + + class Test(BaseModel): + Fruit: Fruit_ | None = None + ``` --- diff --git a/docs/cli-reference/quick-reference.md b/docs/cli-reference/quick-reference.md index 1a9d38d47..1d4b299bc 100644 --- a/docs/cli-reference/quick-reference.md +++ b/docs/cli-reference/quick-reference.md @@ -56,7 +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) | Test field-type collision with rename-type strategy. | +| [`--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. | @@ -205,7 +205,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) - Test field-type collision with rename-type strategy. +- [`--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... From 63cfb529f9e5daa8f1a161058c52ed4ab0bc7328 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 22 Dec 2025 11:53:45 +0000 Subject: [PATCH 6/8] Remove inline comment --- src/datamodel_code_generator/parser/base.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 4e3af61c7..85d95fd5a 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -1866,9 +1866,6 @@ def __change_field_name( colliding_reference = data_type.reference if colliding_reference is not None: - # When a field name collides with a type name, the type's reference source - # is always a DataModel (Enum, model class, etc.) because DataModel.__init__ - # sets reference.source = self source = cast("DataModel", colliding_reference.source) source.class_name = f"{source.class_name}_" else: From 5c5b5cd611e4b0f7811759e8aecb73b287854eea Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 22 Dec 2025 12:03:33 +0000 Subject: [PATCH 7/8] Use ModelResolver for type renaming consistency --- src/datamodel_code_generator/parser/base.py | 10 ++++++---- .../jsonschema/field_type_collision_rename_type.py | 4 ++-- .../field_type_collision_rename_type_multi_model.py | 6 +++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 85d95fd5a..52dc0d22c 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -1856,20 +1856,22 @@ def __change_field_name( 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 not data_type.reference: continue - filed_name_resolver.exclude_names.add(data_type.reference.short_name) + 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) - source.class_name = f"{source.class_name}_" + 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 = filed_name_resolver.add(["field"], cast("str", filed_name)).name + 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 diff --git a/tests/data/expected/main/jsonschema/field_type_collision_rename_type.py b/tests/data/expected/main/jsonschema/field_type_collision_rename_type.py index 056f8c829..ebc256bbf 100644 --- a/tests/data/expected/main/jsonschema/field_type_collision_rename_type.py +++ b/tests/data/expected/main/jsonschema/field_type_collision_rename_type.py @@ -9,10 +9,10 @@ from pydantic import BaseModel -class Fruit_(Enum): +class Fruit_1(Enum): apple = 'apple' banana = 'banana' class Test(BaseModel): - Fruit: Fruit_ | None = None + Fruit: Fruit_1 | None = None diff --git a/tests/data/expected/main/jsonschema/field_type_collision_rename_type_multi_model.py b/tests/data/expected/main/jsonschema/field_type_collision_rename_type_multi_model.py index 84a282470..56c7f83e3 100644 --- a/tests/data/expected/main/jsonschema/field_type_collision_rename_type_multi_model.py +++ b/tests/data/expected/main/jsonschema/field_type_collision_rename_type_multi_model.py @@ -9,15 +9,15 @@ from pydantic import BaseModel -class Fruit_(Enum): +class Fruit_1(Enum): apple = 'apple' banana = 'banana' class Child(BaseModel): - Fruit: Fruit_ | None = None + Fruit: Fruit_1 | None = None class Parent(BaseModel): - Fruit: Fruit_ | None = None + Fruit: Fruit_1 | None = None child: Child | None = None From 943fb8faf245aa4ece0487485dca6413253c2029 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 22 Dec 2025 12:04:23 +0000 Subject: [PATCH 8/8] docs: update CLI reference documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by GitHub Actions --- docs/cli-reference/field-customization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cli-reference/field-customization.md b/docs/cli-reference/field-customization.md index 7c15c3aab..67336098a 100644 --- a/docs/cli-reference/field-customization.md +++ b/docs/cli-reference/field-customization.md @@ -1868,13 +1868,13 @@ the resolution strategy: from pydantic import BaseModel - class Fruit_(Enum): + class Fruit_1(Enum): apple = 'apple' banana = 'banana' class Test(BaseModel): - Fruit: Fruit_ | None = None + Fruit: Fruit_1 | None = None ``` ---