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
13 changes: 10 additions & 3 deletions docs/cli-reference/field-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -3759,10 +3759,17 @@ This is useful when schemas have descriptive titles that should be preserved.
)


class ProcessingStatusUnionTitle(BaseModel):
__root__: (
ProcessingStatusDetail | ExtendedProcessingTask | ProcessingStatusTitle
) = Field(..., title='Processing Status Union Title')


class ProcessingTaskTitle(BaseModel):
processing_status_union: (
ProcessingStatusDetail | ExtendedProcessingTask | ProcessingStatusTitle | None
) = Field('COMPLETED', title='Processing Status Union Title')
processing_status_union: ProcessingStatusUnionTitle | None = Field(
default_factory=lambda: ProcessingStatusUnionTitle.parse_obj('COMPLETED'),
title='Processing Status Union Title',
)
processing_status: ProcessingStatusTitle | None = 'COMPLETED'
name: str | None = None
kind: Kind | None = None
Expand Down
14 changes: 9 additions & 5 deletions src/datamodel_code_generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,13 +457,17 @@ def _create_parser_config(
) -> _ConfigT:
"""Create a parser config from GenerateConfig with additional options.

For Pydantic v2: Uses model_validate with extra='ignore' and model_copy.
For Pydantic v1: Uses dict comprehension to filter fields.
Filters GenerateConfig fields to only those expected by the parser config class,
then merges with additional_options.
"""
if is_pydantic_v2():
return config_class.model_validate(generate_config, from_attributes=True, extra="ignore").model_copy(
update=additional_options
)
parser_config_fields = set(config_class.model_fields.keys())
all_options = {
k: v
for k, v in generate_config.model_dump().items()
if k in parser_config_fields and k not in additional_options
} | dict(additional_options)
return config_class.model_validate(all_options)
parser_config_fields = set(config_class.__fields__.keys())
all_options = {
k: v for k, v in generate_config.dict().items() if k in parser_config_fields and k not in additional_options
Expand Down
46 changes: 45 additions & 1 deletion src/datamodel_code_generator/parser/jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2636,6 +2636,41 @@ def parse_property_names( # noqa: PLR0912
dict_key=key_type,
)

def _should_create_type_alias_for_title(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.
"""
if not (self.use_title_as_name and item.title):
return False

if item.is_array:
return True
if item.anyOf or item.oneOf:
combined_items = item.anyOf or item.oneOf
const_enum_data = self._extract_const_enum_from_combined(combined_items, item.type)
if const_enum_data is None:
return True
enum_values, varnames, enum_type, nullable = const_enum_data
synthetic_obj = self._create_synthetic_enum_obj(item, enum_values, varnames, enum_type, nullable)
if self.should_parse_enum_as_literal(synthetic_obj, property_name=name, property_obj=item):
return True
if (
item.is_object
and not item.properties
and not item.patternProperties
and not item.propertyNames
and isinstance(item.additionalProperties, JsonSchemaObject)
):
return True
return bool(
item.enum
and not self.ignore_enum_constraints
and self.should_parse_enum_as_literal(item, property_name=name)
)

def parse_item( # noqa: PLR0911, PLR0912, PLR0914
self,
name: str,
Expand All @@ -2651,6 +2686,8 @@ def parse_item( # noqa: PLR0911, PLR0912, PLR0914
if self.use_title_as_name and item.title:
name = sanitize_module_name(item.title, treat_dot_as_module=self.treat_dot_as_module)
singular_name = False
if self._should_create_type_alias_for_title(item, name):
return self.parse_root_type(name, item, path)
if parent and not item.enum and item.has_constraint and (parent.has_constraint or self.field_constraints):
root_type_path = get_special_path("array", path)
return self.parse_root_type(
Expand Down Expand Up @@ -2893,7 +2930,7 @@ def parse_array(
self.results.append(data_model_root)
return self.data_type(reference=reference)

def parse_root_type( # noqa: PLR0912, PLR0915
def parse_root_type( # noqa: PLR0912, PLR0914, PLR0915
self,
name: str,
obj: JsonSchemaObject,
Expand Down Expand Up @@ -2940,6 +2977,13 @@ def parse_root_type( # noqa: PLR0912, PLR0915
data_type = self.parse_property_names(
name, obj.propertyNames, obj.additionalProperties, path, parent_obj=obj
)
elif obj.is_object and not obj.properties and isinstance(obj.additionalProperties, JsonSchemaObject):
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)],
**dict_flags,
)
elif obj.enum and not self.ignore_enum_constraints:
if self.should_parse_enum_as_literal(obj, property_name=name):
data_type = self.parse_enum_as_literal(obj)
Expand Down
13 changes: 10 additions & 3 deletions tests/data/expected/main/jsonschema/titles_use_title_as_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,17 @@ class ExtendedProcessingTasksTitle(BaseModel):
)


class ProcessingStatusUnionTitle(BaseModel):
__root__: (
ProcessingStatusDetail | ExtendedProcessingTask | ProcessingStatusTitle
) = Field(..., title='Processing Status Union Title')


class ProcessingTaskTitle(BaseModel):
processing_status_union: (
ProcessingStatusDetail | ExtendedProcessingTask | ProcessingStatusTitle | None
) = Field('COMPLETED', title='Processing Status Union Title')
processing_status_union: ProcessingStatusUnionTitle | None = Field(
default_factory=lambda: ProcessingStatusUnionTitle.parse_obj('COMPLETED'),
title='Processing Status Union Title',
)
processing_status: ProcessingStatusTitle | None = 'COMPLETED'
name: str | None = None
kind: Kind | None = None
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# generated by datamodel-codegen:
# filename: use_title_as_name_inline_types.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from typing import Literal, NotRequired, TypedDict

type MyArrayName = list[str]


type MyObjectName = dict[str, str]


type MyEnumName = Literal['foo', 'bar']


type MyOneOfName = str | float


type MyAnyOfName = bool | int


type MyOneOfConstName = Literal['alpha', 'beta']


class Foo(TypedDict):
array: NotRequired[MyArrayName]
object: NotRequired[MyObjectName]
enum: NotRequired[MyEnumName]
oneOf: NotRequired[MyOneOfName]
anyOf: NotRequired[MyAnyOfName]
oneOfConst: NotRequired[MyOneOfConstName]
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# generated by datamodel-codegen:
# filename: use_title_as_name_inline_types.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from enum import Enum, StrEnum
from typing import Any

from pydantic import BaseModel, Field, RootModel


class Model(RootModel[Any]):
root: Any


class MyArrayName(RootModel[list[str]]):
root: list[str] = Field(..., title='MyArrayName')


class MyObjectName(RootModel[dict[str, str]]):
root: dict[str, str] = Field(..., title='MyObjectName')


class MyEnumName(Enum):
foo = 'foo'
bar = 'bar'


class MyOneOfName(RootModel[str | float]):
root: str | float = Field(..., title='MyOneOfName')


class MyAnyOfName(RootModel[bool | int]):
root: bool | int = Field(..., title='MyAnyOfName')


class MyOneOfConstName(StrEnum):
alpha = 'alpha'
beta = 'beta'


class Foo(BaseModel):
array: MyArrayName | None = Field(None, title='MyArrayName')
object: MyObjectName | None = Field(None, title='MyObjectName')
enum: MyEnumName | None = Field(None, title='MyEnumName')
oneOf: MyOneOfName | None = Field(None, title='MyOneOfName')
anyOf: MyAnyOfName | None = Field(None, title='MyAnyOfName')
oneOfConst: MyOneOfConstName | None = Field(None, title='MyOneOfConstName')
61 changes: 61 additions & 0 deletions tests/data/jsonschema/use_title_as_name_inline_types.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"Foo": {
"type": "object",
"properties": {
"array": {
"title": "MyArrayName",
"type": "array",
"items": {
"type": "string"
}
},
"object": {
"title": "MyObjectName",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"enum": {
"title": "MyEnumName",
"enum": ["foo", "bar"]
},
"oneOf": {
"title": "MyOneOfName",
"oneOf": [
{
"type": "string"
},
{
"type": "number"
}
]
},
"anyOf": {
"title": "MyAnyOfName",
"anyOf": [
{
"type": "boolean"
},
{
"type": "integer"
}
]
},
"oneOfConst": {
"title": "MyOneOfConstName",
"oneOf": [
{
"const": "alpha"
},
{
"const": "beta"
}
]
}
}
}
}
}
54 changes: 54 additions & 0 deletions tests/main/jsonschema/test_main_jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3050,6 +3050,60 @@ def test_jsonschema_title_with_dots(output_file: Path) -> None:
)


@BLACK_PY313_SKIP
def test_jsonschema_use_title_as_name_inline_types(output_file: Path) -> None:
"""Test use-title-as-name creates type aliases for inline types.

When use_title_as_name is enabled and inline types (array, dict, oneOf, anyOf, enum)
have a title, type aliases should be created instead of using inline types directly.

Fixes: https://github.com/koxudaxi/datamodel-code-generator/issues/2887
"""
run_main_and_assert(
input_path=JSON_SCHEMA_DATA_PATH / "use_title_as_name_inline_types.json",
output_path=output_file,
input_file_type="jsonschema",
assert_func=assert_file_content,
expected_file="use_title_as_name_inline_types.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_inline_types_pydantic(output_file: Path) -> None:
"""Test use-title-as-name with Pydantic v2 creates named types for inline types.

This test covers the case where should_parse_enum_as_literal returns False
(for oneOf with const values), exercising the False branch in
_should_create_type_alias_for_title.
"""
run_main_and_assert(
input_path=JSON_SCHEMA_DATA_PATH / "use_title_as_name_inline_types.json",
output_path=output_file,
input_file_type="jsonschema",
assert_func=assert_file_content,
expected_file="use_title_as_name_inline_types_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