diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 00ab39444..cf7b7c622 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -1891,7 +1891,7 @@ def parse_combined_schema( target_attribute_name: str, ) -> list[DataType]: """Parse combined schema (anyOf, oneOf, allOf) into a list of data types.""" - base_object = model_dump(obj, exclude={target_attribute_name}, exclude_unset=True, by_alias=True) + base_object = model_dump(obj, exclude={target_attribute_name, "title"}, exclude_unset=True, by_alias=True) combined_schemas: list[JsonSchemaObject] = [] refs = [] for index, target_attribute in enumerate(getattr(obj, target_attribute_name, [])): @@ -2636,12 +2636,14 @@ def parse_property_names( # noqa: PLR0912 dict_key=key_type, ) - def _should_create_type_alias_for_title(self, item: JsonSchemaObject, name: str) -> bool: + def _should_create_type_alias_for_title( # noqa: PLR0911 + self, item: JsonSchemaObject, name: str + ) -> bool: """Check if a type alias should be created for an inline type with title. When use_title_as_name is enabled and the item has a title, certain inline types - (array, dict, oneOf/anyOf unions, enum as literal) should create a type alias - instead of being inlined. + (array, dict, oneOf/anyOf unions, enum as literal, primitive types) should create + a type alias instead of being inlined. """ if not (self.use_title_as_name and item.title): return False @@ -2665,11 +2667,27 @@ def _should_create_type_alias_for_title(self, item: JsonSchemaObject, name: str) and isinstance(item.additionalProperties, JsonSchemaObject) ): return True - return bool( + if item.patternProperties: + return True + if item.propertyNames: + return True + if ( item.enum and not self.ignore_enum_constraints and self.should_parse_enum_as_literal(item, property_name=name) + ): + return True + is_primitive = ( + item.type + and not item.is_array + and not item.is_object + and not item.anyOf + and not item.oneOf + and not item.allOf + and not item.ref + and not (item.enum and not self.ignore_enum_constraints) ) + return bool(is_primitive) def parse_item( # noqa: PLR0911, PLR0912, PLR0914 self, @@ -2763,7 +2781,11 @@ def parse_item( # noqa: PLR0911, PLR0912, PLR0914 python_type_flags = self._get_python_type_flags(item) dict_flags = python_type_flags or {"is_dict": True} return self.data_type( - data_types=[self.parse_item(name, item.additionalProperties, object_path)], + data_types=[ + self.parse_item( + name, item.additionalProperties, get_special_path("additionalProperties", object_path) + ) + ], **dict_flags, ) return self.data_type_manager.get_data_type( @@ -2981,7 +3003,9 @@ def parse_root_type( # noqa: PLR0912, PLR0914, PLR0915 python_type_flags = self._get_python_type_flags(obj) dict_flags = python_type_flags or {"is_dict": True} data_type = self.data_type( - data_types=[self.parse_item(name, obj.additionalProperties, path)], + data_types=[ + self.parse_item(name, obj.additionalProperties, get_special_path("additionalProperties", path)) + ], **dict_flags, ) elif obj.enum and not self.ignore_enum_constraints: diff --git a/tests/data/expected/main/jsonschema/use_title_as_name_nested_titles.py b/tests/data/expected/main/jsonschema/use_title_as_name_nested_titles.py new file mode 100644 index 000000000..ca23cca53 --- /dev/null +++ b/tests/data/expected/main/jsonschema/use_title_as_name_nested_titles.py @@ -0,0 +1,61 @@ +# generated by datamodel-codegen: +# filename: use_title_as_name_nested_titles.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import NotRequired, TypedDict + +type MyArrayItem = str + + +type MyArray = list[MyArrayItem] + + +type MyObjectProp = str + + +type MyObject = dict[str, MyObjectProp] + + +type MyOneOfBranch = str + + +type MyOneOf = MyOneOfBranch | float + + +type MyAnyOfBranch = bool + + +type MyAnyOf = MyAnyOfBranch | int + + +type MyDeepItem = int + + +type MyNestedArrayItem = list[MyDeepItem] + + +type MyNestedArray = list[MyNestedArrayItem] + + +type MyPatternValue = str + + +type MyPatternObj = dict[str, MyPatternValue] + + +type MyPropValue = int + + +type MyPropNamesObj = dict[str, MyPropValue] + + +class Foo(TypedDict): + array: NotRequired[MyArray] + object: NotRequired[MyObject] + oneOf: NotRequired[MyOneOf] + anyOf: NotRequired[MyAnyOf] + nestedArray: NotRequired[MyNestedArray] + patternObj: NotRequired[MyPatternObj] + propNamesObj: NotRequired[MyPropNamesObj] diff --git a/tests/data/expected/main/jsonschema/use_title_as_name_nested_titles_pydantic.py b/tests/data/expected/main/jsonschema/use_title_as_name_nested_titles_pydantic.py new file mode 100644 index 000000000..eae49e0ed --- /dev/null +++ b/tests/data/expected/main/jsonschema/use_title_as_name_nested_titles_pydantic.py @@ -0,0 +1,87 @@ +# generated by datamodel-codegen: +# filename: use_title_as_name_nested_titles.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, Field, RootModel, constr + + +class Model(RootModel[Any]): + root: Any + + +class MyArrayItem(RootModel[str]): + root: str = Field(..., title='MyArrayItem') + + +class MyArray(RootModel[list[MyArrayItem]]): + root: list[MyArrayItem] = Field(..., title='MyArray') + + +class MyObjectProp(RootModel[str]): + root: str = Field(..., title='MyObjectProp') + + +class MyObject(RootModel[dict[str, MyObjectProp]]): + root: dict[str, MyObjectProp] = Field(..., title='MyObject') + + +class MyOneOfBranch(RootModel[str]): + root: str = Field(..., title='MyOneOfBranch') + + +class MyOneOf(RootModel[MyOneOfBranch | float]): + root: MyOneOfBranch | float = Field(..., title='MyOneOf') + + +class MyAnyOfBranch(RootModel[bool]): + root: bool = Field(..., title='MyAnyOfBranch') + + +class MyAnyOf(RootModel[MyAnyOfBranch | int]): + root: MyAnyOfBranch | int = Field(..., title='MyAnyOf') + + +class MyDeepItem(RootModel[int]): + root: int = Field(..., title='MyDeepItem') + + +class MyNestedArrayItem(RootModel[list[MyDeepItem]]): + root: list[MyDeepItem] = Field(..., title='MyNestedArrayItem') + + +class MyNestedArray(RootModel[list[MyNestedArrayItem]]): + root: list[MyNestedArrayItem] = Field(..., title='MyNestedArray') + + +class MyPatternValue(RootModel[str]): + root: str = Field(..., title='MyPatternValue') + + +class MyPatternObj(RootModel[dict[constr(pattern=r'^S_'), MyPatternValue]]): + root: dict[constr(pattern=r'^S_'), MyPatternValue] = Field( + ..., title='MyPatternObj' + ) + + +class MyPropValue(RootModel[int]): + root: int = Field(..., title='MyPropValue') + + +class MyPropNamesObj(RootModel[dict[constr(pattern=r'^[a-z]+$'), MyPropValue]]): + root: dict[constr(pattern=r'^[a-z]+$'), MyPropValue] = Field( + ..., title='MyPropNamesObj' + ) + + +class Foo(BaseModel): + array: MyArray | None = Field(None, title='MyArray') + object: MyObject | None = Field(None, title='MyObject') + oneOf: MyOneOf | None = Field(None, title='MyOneOf') + anyOf: MyAnyOf | None = Field(None, title='MyAnyOf') + nestedArray: MyNestedArray | None = Field(None, title='MyNestedArray') + patternObj: MyPatternObj | None = Field(None, title='MyPatternObj') + propNamesObj: MyPropNamesObj | None = Field(None, title='MyPropNamesObj') diff --git a/tests/data/jsonschema/use_title_as_name_nested_titles.json b/tests/data/jsonschema/use_title_as_name_nested_titles.json new file mode 100644 index 000000000..2d1db61b2 --- /dev/null +++ b/tests/data/jsonschema/use_title_as_name_nested_titles.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Foo": { + "type": "object", + "properties": { + "array": { + "title": "MyArray", + "type": "array", + "items": { + "title": "MyArrayItem", + "type": "string" + } + }, + "object": { + "title": "MyObject", + "type": "object", + "additionalProperties": { + "title": "MyObjectProp", + "type": "string" + } + }, + "oneOf": { + "title": "MyOneOf", + "oneOf": [ + { + "title": "MyOneOfBranch", + "type": "string" + }, + { + "type": "number" + } + ] + }, + "anyOf": { + "title": "MyAnyOf", + "anyOf": [ + { + "title": "MyAnyOfBranch", + "type": "boolean" + }, + { + "type": "integer" + } + ] + }, + "nestedArray": { + "title": "MyNestedArray", + "type": "array", + "items": { + "title": "MyNestedArrayItem", + "type": "array", + "items": { + "title": "MyDeepItem", + "type": "integer" + } + } + }, + "patternObj": { + "title": "MyPatternObj", + "type": "object", + "patternProperties": { + "^S_": { + "title": "MyPatternValue", + "type": "string" + } + } + }, + "propNamesObj": { + "title": "MyPropNamesObj", + "type": "object", + "propertyNames": { + "pattern": "^[a-z]+$" + }, + "additionalProperties": { + "title": "MyPropValue", + "type": "integer" + } + } + } + } + } +} diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index 925f1cc80..27a51c239 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -3104,6 +3104,56 @@ def test_jsonschema_use_title_as_name_inline_types_pydantic(output_file: Path) - ) +@BLACK_PY313_SKIP +def test_jsonschema_use_title_as_name_nested_titles(output_file: Path) -> None: + """Test use-title-as-name creates type aliases for nested elements with titles. + + When use_title_as_name is enabled, nested elements like array items, + additionalProperties values, and oneOf/anyOf branches that have their own + titles should also create type aliases. + + Fixes: https://github.com/koxudaxi/datamodel-code-generator/issues/2887 + """ + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "use_title_as_name_nested_titles.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="use_title_as_name_nested_titles.py", + extra_args=[ + "--use-title-as-name", + "--output-model-type", + "typing.TypedDict", + "--target-python-version", + "3.13", + "--use-union-operator", + "--use-standard-collections", + "--skip-root-model", + ], + ) + + +@BLACK_PY313_SKIP +def test_jsonschema_use_title_as_name_nested_titles_pydantic(output_file: Path) -> None: + """Test use-title-as-name with Pydantic v2 creates named types for nested elements.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "use_title_as_name_nested_titles.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="use_title_as_name_nested_titles_pydantic.py", + extra_args=[ + "--use-title-as-name", + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-python-version", + "3.13", + "--use-union-operator", + "--use-standard-collections", + ], + ) + + def test_main_jsonschema_has_default_value(output_file: Path) -> None: """Test default value handling.""" run_main_and_assert(