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
38 changes: 31 additions & 7 deletions src/datamodel_code_generator/parser/jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, [])):
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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]
Original file line number Diff line number Diff line change
@@ -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')
83 changes: 83 additions & 0 deletions tests/data/jsonschema/use_title_as_name_nested_titles.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
}
50 changes: 50 additions & 0 deletions tests/main/jsonschema/test_main_jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading