From c80e0e607694715f1aee975044dfadb921838931 Mon Sep 17 00:00:00 2001 From: Keyan Zhang <2268452+keyz@users.noreply.github.com> Date: Sat, 7 Mar 2026 23:25:23 -0800 Subject: [PATCH 01/13] new fixtures --- ...pe_alias_chain_model_default_object_ref.py | 25 ++ ...pe_alias_chain_union_default_object_ref.py | 32 ++ ...ype_alias_dict_union_default_object_ref.py | 32 ++ ..._alias_inline_dict_union_default_object.py | 29 ++ ..._alias_inline_list_union_default_object.py | 29 ++ .../type_alias_inline_union_default_object.py | 23 ++ ...lias_inline_union_default_object_one_of.py | 26 ++ ...nion_default_object_silent_wrong_branch.py | 29 ++ ...ype_alias_list_model_default_object_ref.py | 22 + ...ype_alias_list_union_default_object_ref.py | 32 ++ .../type_alias_model_default_object_ref.py | 22 + .../type_alias_union_default_object_ref.py | 26 ++ ...e_alias_union_default_object_ref_any_of.py | 27 ++ ...s_union_default_object_ref_mixed_scalar.py | 28 ++ ...e_alias_union_default_object_ref_one_of.py | 27 ++ ..._alias_chain_model_default_object_ref.json | 25 ++ ..._alias_chain_union_default_object_ref.json | 41 ++ ...e_alias_dict_union_default_object_ref.json | 50 +++ ...lias_inline_dict_union_default_object.json | 47 +++ ...lias_inline_list_union_default_object.json | 47 +++ ...ype_alias_inline_union_default_object.json | 33 ++ ...as_inline_union_default_object_one_of.json | 35 ++ ...on_default_object_silent_wrong_branch.json | 42 ++ ...e_alias_list_model_default_object_ref.json | 27 ++ ...e_alias_list_union_default_object_ref.json | 50 +++ .../type_alias_model_default_object_ref.json | 22 + .../type_alias_union_default_object_ref.json | 36 ++ ...alias_union_default_object_ref_any_of.json | 38 ++ ...union_default_object_ref_mixed_scalar.json | 37 ++ ...alias_union_default_object_ref_one_of.json | 38 ++ tests/main/jsonschema/test_main_jsonschema.py | 376 ++++++++++++++++++ 31 files changed, 1353 insertions(+) create mode 100644 tests/data/expected/main/jsonschema/type_alias_chain_model_default_object_ref.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_chain_union_default_object_ref.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_dict_union_default_object_ref.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_inline_dict_union_default_object.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_inline_list_union_default_object.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_inline_union_default_object.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_one_of.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_silent_wrong_branch.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_list_model_default_object_ref.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_list_union_default_object_ref.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_model_default_object_ref.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_union_default_object_ref.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_any_of.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_mixed_scalar.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_one_of.py create mode 100644 tests/data/jsonschema/type_alias_chain_model_default_object_ref.json create mode 100644 tests/data/jsonschema/type_alias_chain_union_default_object_ref.json create mode 100644 tests/data/jsonschema/type_alias_dict_union_default_object_ref.json create mode 100644 tests/data/jsonschema/type_alias_inline_dict_union_default_object.json create mode 100644 tests/data/jsonschema/type_alias_inline_list_union_default_object.json create mode 100644 tests/data/jsonschema/type_alias_inline_union_default_object.json create mode 100644 tests/data/jsonschema/type_alias_inline_union_default_object_one_of.json create mode 100644 tests/data/jsonschema/type_alias_inline_union_default_object_silent_wrong_branch.json create mode 100644 tests/data/jsonschema/type_alias_list_model_default_object_ref.json create mode 100644 tests/data/jsonschema/type_alias_list_union_default_object_ref.json create mode 100644 tests/data/jsonschema/type_alias_model_default_object_ref.json create mode 100644 tests/data/jsonschema/type_alias_union_default_object_ref.json create mode 100644 tests/data/jsonschema/type_alias_union_default_object_ref_any_of.json create mode 100644 tests/data/jsonschema/type_alias_union_default_object_ref_mixed_scalar.json create mode 100644 tests/data/jsonschema/type_alias_union_default_object_ref_one_of.json diff --git a/tests/data/expected/main/jsonschema/type_alias_chain_model_default_object_ref.py b/tests/data/expected/main/jsonschema/type_alias_chain_model_default_object_ref.py new file mode 100644 index 000000000..513a02386 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_chain_model_default_object_ref.py @@ -0,0 +1,25 @@ +# generated by datamodel-codegen: +# filename: type_alias_chain_model_default_object_ref.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +type Alias1 = A + + +type Alias2 = Alias1 + + +class Model(BaseModel): + x: Alias2 | None = Field( + default_factory=lambda: TypeAdapter(Alias2).validate_python({'type': 'a'}) + ) diff --git a/tests/data/expected/main/jsonschema/type_alias_chain_union_default_object_ref.py b/tests/data/expected/main/jsonschema/type_alias_chain_union_default_object_ref.py new file mode 100644 index 000000000..6454ab364 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_chain_union_default_object_ref.py @@ -0,0 +1,32 @@ +# generated by datamodel-codegen: +# filename: type_alias_chain_union_default_object_ref.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +class B(BaseModel): + type: Literal['b'] = 'b' + + +type Alias1 = A | B + + +type Alias2 = Alias1 + + +class Model(BaseModel): + x_value: Annotated[ + Alias2 | None, + Field( + default_factory=lambda: TypeAdapter(Alias2).validate_python({'type': 'b'}) + ), + ] diff --git a/tests/data/expected/main/jsonschema/type_alias_dict_union_default_object_ref.py b/tests/data/expected/main/jsonschema/type_alias_dict_union_default_object_ref.py new file mode 100644 index 000000000..d0c1b2935 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_dict_union_default_object_ref.py @@ -0,0 +1,32 @@ +# generated by datamodel-codegen: +# filename: type_alias_dict_union_default_object_ref.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +class B(BaseModel): + type: Literal['b'] + value: int | None = None + + +type Alias = dict[str, A | B] + + +class Model(BaseModel): + x_value: Annotated[ + Alias | None, + Field( + default_factory=lambda: TypeAdapter(Alias).validate_python( + {'k': {'type': 'b', 'value': 1}} + ) + ), + ] diff --git a/tests/data/expected/main/jsonschema/type_alias_inline_dict_union_default_object.py b/tests/data/expected/main/jsonschema/type_alias_inline_dict_union_default_object.py new file mode 100644 index 000000000..6a440b719 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_inline_dict_union_default_object.py @@ -0,0 +1,29 @@ +# generated by datamodel-codegen: +# filename: type_alias_inline_dict_union_default_object.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +class B(BaseModel): + type: Literal['b'] + value: int | None = None + + +class Model(BaseModel): + x_value: Annotated[ + dict[str, A | B] | None, + Field( + default_factory=lambda: TypeAdapter(dict[str, A | B]).validate_python( + {'k': {'type': 'b', 'value': 1}} + ) + ), + ] diff --git a/tests/data/expected/main/jsonschema/type_alias_inline_list_union_default_object.py b/tests/data/expected/main/jsonschema/type_alias_inline_list_union_default_object.py new file mode 100644 index 000000000..56bd5e7e8 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_inline_list_union_default_object.py @@ -0,0 +1,29 @@ +# generated by datamodel-codegen: +# filename: type_alias_inline_list_union_default_object.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +class B(BaseModel): + type: Literal['b'] + value: int | None = None + + +class Model(BaseModel): + x_value: Annotated[ + list[A | B] | None, + Field( + default_factory=lambda: TypeAdapter(list[A | B]).validate_python( + [{'type': 'b', 'value': 1}] + ) + ), + ] diff --git a/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object.py b/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object.py new file mode 100644 index 000000000..3b58689e4 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object.py @@ -0,0 +1,23 @@ +# generated by datamodel-codegen: +# filename: type_alias_inline_union_default_object.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +class B(BaseModel): + type: Literal['b'] = 'b' + + +class Model(BaseModel): + x: A | B | None = Field( + default_factory=lambda: TypeAdapter(A | B).validate_python({'type': 'b'}) + ) diff --git a/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_one_of.py b/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_one_of.py new file mode 100644 index 000000000..e083b4d74 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_one_of.py @@ -0,0 +1,26 @@ +# generated by datamodel-codegen: +# filename: type_alias_inline_union_default_object_one_of.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +class B(BaseModel): + type: Literal['b'] = 'b' + + +class Model(BaseModel): + x_value: Annotated[ + A | B | None, + Field( + default_factory=lambda: TypeAdapter(A | B).validate_python({'type': 'b'}) + ), + ] diff --git a/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_silent_wrong_branch.py b/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_silent_wrong_branch.py new file mode 100644 index 000000000..7916410d0 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_silent_wrong_branch.py @@ -0,0 +1,29 @@ +# generated by datamodel-codegen: +# filename: type_alias_inline_union_default_object_silent_wrong_branch.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + note: str | None = None + + +class B(BaseModel): + type: Literal['b'] + value: int | None = None + + +class Model(BaseModel): + x_value: Annotated[ + A | B | None, + Field( + default_factory=lambda: TypeAdapter(A | B).validate_python( + {'type': 'b', 'value': 1} + ) + ), + ] diff --git a/tests/data/expected/main/jsonschema/type_alias_list_model_default_object_ref.py b/tests/data/expected/main/jsonschema/type_alias_list_model_default_object_ref.py new file mode 100644 index 000000000..3f2807a7a --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_list_model_default_object_ref.py @@ -0,0 +1,22 @@ +# generated by datamodel-codegen: +# filename: type_alias_list_model_default_object_ref.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +type Alias = list[A] + + +class Model(BaseModel): + x: Alias | None = Field( + default_factory=lambda: TypeAdapter(Alias).validate_python([{'type': 'a'}]) + ) diff --git a/tests/data/expected/main/jsonschema/type_alias_list_union_default_object_ref.py b/tests/data/expected/main/jsonschema/type_alias_list_union_default_object_ref.py new file mode 100644 index 000000000..c0075a848 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_list_union_default_object_ref.py @@ -0,0 +1,32 @@ +# generated by datamodel-codegen: +# filename: type_alias_list_union_default_object_ref.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +class B(BaseModel): + type: Literal['b'] + value: int | None = None + + +type Alias = list[A | B] + + +class Model(BaseModel): + x_value: Annotated[ + Alias | None, + Field( + default_factory=lambda: TypeAdapter(Alias).validate_python( + [{'type': 'b', 'value': 1}] + ) + ), + ] diff --git a/tests/data/expected/main/jsonschema/type_alias_model_default_object_ref.py b/tests/data/expected/main/jsonschema/type_alias_model_default_object_ref.py new file mode 100644 index 000000000..455bb9a0c --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_model_default_object_ref.py @@ -0,0 +1,22 @@ +# generated by datamodel-codegen: +# filename: type_alias_model_default_object_ref.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +type Alias = A + + +class Model(BaseModel): + x: Alias | None = Field( + default_factory=lambda: TypeAdapter(Alias).validate_python({'type': 'a'}) + ) diff --git a/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref.py b/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref.py new file mode 100644 index 000000000..a4d6de172 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref.py @@ -0,0 +1,26 @@ +# generated by datamodel-codegen: +# filename: type_alias_union_default_object_ref.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +class B(BaseModel): + type: Literal['b'] = 'b' + + +type U = A | B + + +class Model(BaseModel): + x: U | None = Field( + default_factory=lambda: TypeAdapter(U).validate_python({'type': 'b'}) + ) diff --git a/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_any_of.py b/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_any_of.py new file mode 100644 index 000000000..9e66ffc07 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_any_of.py @@ -0,0 +1,27 @@ +# generated by datamodel-codegen: +# filename: type_alias_union_default_object_ref_any_of.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +class B(BaseModel): + type: Literal['b'] = 'b' + + +type U = A | B + + +class Model(BaseModel): + x_value: Annotated[ + U | None, + Field(default_factory=lambda: TypeAdapter(U).validate_python({'type': 'b'})), + ] diff --git a/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_mixed_scalar.py b/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_mixed_scalar.py new file mode 100644 index 000000000..cda85e451 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_mixed_scalar.py @@ -0,0 +1,28 @@ +# generated by datamodel-codegen: +# filename: type_alias_union_default_object_ref_mixed_scalar.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class B(BaseModel): + type: Literal['b'] + value: int | None = None + + +type Alias = str | B + + +class Model(BaseModel): + x_value: Annotated[ + Alias | None, + Field( + default_factory=lambda: TypeAdapter(Alias).validate_python( + {'type': 'b', 'value': 1} + ) + ), + ] diff --git a/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_one_of.py b/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_one_of.py new file mode 100644 index 000000000..4e8800298 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_one_of.py @@ -0,0 +1,27 @@ +# generated by datamodel-codegen: +# filename: type_alias_union_default_object_ref_one_of.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated, Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +class B(BaseModel): + type: Literal['b'] = 'b' + + +type U = A | B + + +class Model(BaseModel): + x_value: Annotated[ + U | None, + Field(default_factory=lambda: TypeAdapter(U).validate_python({'type': 'b'})), + ] diff --git a/tests/data/jsonschema/type_alias_chain_model_default_object_ref.json b/tests/data/jsonschema/type_alias_chain_model_default_object_ref.json new file mode 100644 index 000000000..5821a1718 --- /dev/null +++ b/tests/data/jsonschema/type_alias_chain_model_default_object_ref.json @@ -0,0 +1,25 @@ +{ + "$defs": { + "A": { + "properties": { + "type": { + "const": "a" + } + } + }, + "Alias1": { + "$ref": "#/$defs/A" + }, + "Alias2": { + "$ref": "#/$defs/Alias1" + } + }, + "properties": { + "x": { + "default": { + "type": "a" + }, + "$ref": "#/$defs/Alias2" + } + } +} diff --git a/tests/data/jsonschema/type_alias_chain_union_default_object_ref.json b/tests/data/jsonschema/type_alias_chain_union_default_object_ref.json new file mode 100644 index 000000000..02fcea3ab --- /dev/null +++ b/tests/data/jsonschema/type_alias_chain_union_default_object_ref.json @@ -0,0 +1,41 @@ +{ + "$defs": { + "A": { + "title": "A", + "properties": { + "type": { + "const": "a" + } + } + }, + "B": { + "title": "B", + "properties": { + "type": { + "const": "b" + } + } + }, + "Alias1": { + "anyOf": [ + { + "$ref": "#/$defs/A" + }, + { + "$ref": "#/$defs/B" + } + ] + }, + "Alias2": { + "$ref": "#/$defs/Alias1" + } + }, + "properties": { + "x_value": { + "default": { + "type": "b" + }, + "$ref": "#/$defs/Alias2" + } + } +} diff --git a/tests/data/jsonschema/type_alias_dict_union_default_object_ref.json b/tests/data/jsonschema/type_alias_dict_union_default_object_ref.json new file mode 100644 index 000000000..a8966576c --- /dev/null +++ b/tests/data/jsonschema/type_alias_dict_union_default_object_ref.json @@ -0,0 +1,50 @@ +{ + "$defs": { + "A": { + "title": "A", + "properties": { + "type": { + "const": "a" + } + } + }, + "B": { + "title": "B", + "properties": { + "type": { + "const": "b" + }, + "value": { + "type": "integer" + } + }, + "required": [ + "type" + ] + }, + "Alias": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/A" + }, + { + "$ref": "#/$defs/B" + } + ] + } + } + }, + "properties": { + "x_value": { + "$ref": "#/$defs/Alias", + "default": { + "k": { + "type": "b", + "value": 1 + } + } + } + } +} diff --git a/tests/data/jsonschema/type_alias_inline_dict_union_default_object.json b/tests/data/jsonschema/type_alias_inline_dict_union_default_object.json new file mode 100644 index 000000000..902abbca1 --- /dev/null +++ b/tests/data/jsonschema/type_alias_inline_dict_union_default_object.json @@ -0,0 +1,47 @@ +{ + "$defs": { + "A": { + "title": "A", + "properties": { + "type": { + "const": "a" + } + } + }, + "B": { + "title": "B", + "properties": { + "type": { + "const": "b" + }, + "value": { + "type": "integer" + } + }, + "required": [ + "type" + ] + } + }, + "properties": { + "x_value": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/A" + }, + { + "$ref": "#/$defs/B" + } + ] + }, + "default": { + "k": { + "type": "b", + "value": 1 + } + } + } + } +} diff --git a/tests/data/jsonschema/type_alias_inline_list_union_default_object.json b/tests/data/jsonschema/type_alias_inline_list_union_default_object.json new file mode 100644 index 000000000..10463a113 --- /dev/null +++ b/tests/data/jsonschema/type_alias_inline_list_union_default_object.json @@ -0,0 +1,47 @@ +{ + "$defs": { + "A": { + "title": "A", + "properties": { + "type": { + "const": "a" + } + } + }, + "B": { + "title": "B", + "properties": { + "type": { + "const": "b" + }, + "value": { + "type": "integer" + } + }, + "required": [ + "type" + ] + } + }, + "properties": { + "x_value": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/A" + }, + { + "$ref": "#/$defs/B" + } + ] + }, + "default": [ + { + "type": "b", + "value": 1 + } + ] + } + } +} diff --git a/tests/data/jsonschema/type_alias_inline_union_default_object.json b/tests/data/jsonschema/type_alias_inline_union_default_object.json new file mode 100644 index 000000000..31af914b7 --- /dev/null +++ b/tests/data/jsonschema/type_alias_inline_union_default_object.json @@ -0,0 +1,33 @@ +{ + "$defs": { + "A": { + "properties": { + "type": { + "const": "a" + } + } + }, + "B": { + "properties": { + "type": { + "const": "b" + } + } + } + }, + "properties": { + "x": { + "default": { + "type": "b" + }, + "anyOf": [ + { + "$ref": "#/$defs/A" + }, + { + "$ref": "#/$defs/B" + } + ] + } + } +} diff --git a/tests/data/jsonschema/type_alias_inline_union_default_object_one_of.json b/tests/data/jsonschema/type_alias_inline_union_default_object_one_of.json new file mode 100644 index 000000000..be83c28d2 --- /dev/null +++ b/tests/data/jsonschema/type_alias_inline_union_default_object_one_of.json @@ -0,0 +1,35 @@ +{ + "$defs": { + "A": { + "title": "A", + "properties": { + "type": { + "const": "a" + } + } + }, + "B": { + "title": "B", + "properties": { + "type": { + "const": "b" + } + } + } + }, + "properties": { + "x_value": { + "default": { + "type": "b" + }, + "oneOf": [ + { + "$ref": "#/$defs/A" + }, + { + "$ref": "#/$defs/B" + } + ] + } + } +} diff --git a/tests/data/jsonschema/type_alias_inline_union_default_object_silent_wrong_branch.json b/tests/data/jsonschema/type_alias_inline_union_default_object_silent_wrong_branch.json new file mode 100644 index 000000000..0b4891d49 --- /dev/null +++ b/tests/data/jsonschema/type_alias_inline_union_default_object_silent_wrong_branch.json @@ -0,0 +1,42 @@ +{ + "$defs": { + "A": { + "title": "A", + "properties": { + "note": { + "type": "string" + } + } + }, + "B": { + "title": "B", + "properties": { + "type": { + "const": "b" + }, + "value": { + "type": "integer" + } + }, + "required": [ + "type" + ] + } + }, + "properties": { + "x_value": { + "default": { + "type": "b", + "value": 1 + }, + "anyOf": [ + { + "$ref": "#/$defs/A" + }, + { + "$ref": "#/$defs/B" + } + ] + } + } +} diff --git a/tests/data/jsonschema/type_alias_list_model_default_object_ref.json b/tests/data/jsonschema/type_alias_list_model_default_object_ref.json new file mode 100644 index 000000000..461badb89 --- /dev/null +++ b/tests/data/jsonschema/type_alias_list_model_default_object_ref.json @@ -0,0 +1,27 @@ +{ + "$defs": { + "A": { + "properties": { + "type": { + "const": "a" + } + } + }, + "Alias": { + "type": "array", + "items": { + "$ref": "#/$defs/A" + } + } + }, + "properties": { + "x": { + "default": [ + { + "type": "a" + } + ], + "$ref": "#/$defs/Alias" + } + } +} diff --git a/tests/data/jsonschema/type_alias_list_union_default_object_ref.json b/tests/data/jsonschema/type_alias_list_union_default_object_ref.json new file mode 100644 index 000000000..446bf92b3 --- /dev/null +++ b/tests/data/jsonschema/type_alias_list_union_default_object_ref.json @@ -0,0 +1,50 @@ +{ + "$defs": { + "A": { + "title": "A", + "properties": { + "type": { + "const": "a" + } + } + }, + "B": { + "title": "B", + "properties": { + "type": { + "const": "b" + }, + "value": { + "type": "integer" + } + }, + "required": [ + "type" + ] + }, + "Alias": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/A" + }, + { + "$ref": "#/$defs/B" + } + ] + } + } + }, + "properties": { + "x_value": { + "$ref": "#/$defs/Alias", + "default": [ + { + "type": "b", + "value": 1 + } + ] + } + } +} diff --git a/tests/data/jsonschema/type_alias_model_default_object_ref.json b/tests/data/jsonschema/type_alias_model_default_object_ref.json new file mode 100644 index 000000000..c18e25e54 --- /dev/null +++ b/tests/data/jsonschema/type_alias_model_default_object_ref.json @@ -0,0 +1,22 @@ +{ + "$defs": { + "A": { + "properties": { + "type": { + "const": "a" + } + } + }, + "Alias": { + "$ref": "#/$defs/A" + } + }, + "properties": { + "x": { + "default": { + "type": "a" + }, + "$ref": "#/$defs/Alias" + } + } +} diff --git a/tests/data/jsonschema/type_alias_union_default_object_ref.json b/tests/data/jsonschema/type_alias_union_default_object_ref.json new file mode 100644 index 000000000..869cfe099 --- /dev/null +++ b/tests/data/jsonschema/type_alias_union_default_object_ref.json @@ -0,0 +1,36 @@ +{ + "$defs": { + "A": { + "properties": { + "type": { + "const": "a" + } + } + }, + "B": { + "properties": { + "type": { + "const": "b" + } + } + }, + "U": { + "anyOf": [ + { + "$ref": "#/$defs/A" + }, + { + "$ref": "#/$defs/B" + } + ] + } + }, + "properties": { + "x": { + "default": { + "type": "b" + }, + "$ref": "#/$defs/U" + } + } +} diff --git a/tests/data/jsonschema/type_alias_union_default_object_ref_any_of.json b/tests/data/jsonschema/type_alias_union_default_object_ref_any_of.json new file mode 100644 index 000000000..445fc50a9 --- /dev/null +++ b/tests/data/jsonschema/type_alias_union_default_object_ref_any_of.json @@ -0,0 +1,38 @@ +{ + "$defs": { + "A": { + "title": "A", + "properties": { + "type": { + "const": "a" + } + } + }, + "B": { + "title": "B", + "properties": { + "type": { + "const": "b" + } + } + }, + "U": { + "anyOf": [ + { + "$ref": "#/$defs/A" + }, + { + "$ref": "#/$defs/B" + } + ] + } + }, + "properties": { + "x_value": { + "default": { + "type": "b" + }, + "$ref": "#/$defs/U" + } + } +} diff --git a/tests/data/jsonschema/type_alias_union_default_object_ref_mixed_scalar.json b/tests/data/jsonschema/type_alias_union_default_object_ref_mixed_scalar.json new file mode 100644 index 000000000..e499b1537 --- /dev/null +++ b/tests/data/jsonschema/type_alias_union_default_object_ref_mixed_scalar.json @@ -0,0 +1,37 @@ +{ + "$defs": { + "B": { + "title": "B", + "properties": { + "type": { + "const": "b" + }, + "value": { + "type": "integer" + } + }, + "required": [ + "type" + ] + }, + "Alias": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/B" + } + ] + } + }, + "properties": { + "x_value": { + "$ref": "#/$defs/Alias", + "default": { + "type": "b", + "value": 1 + } + } + } +} diff --git a/tests/data/jsonschema/type_alias_union_default_object_ref_one_of.json b/tests/data/jsonschema/type_alias_union_default_object_ref_one_of.json new file mode 100644 index 000000000..9f48604c3 --- /dev/null +++ b/tests/data/jsonschema/type_alias_union_default_object_ref_one_of.json @@ -0,0 +1,38 @@ +{ + "$defs": { + "A": { + "title": "A", + "properties": { + "type": { + "const": "a" + } + } + }, + "B": { + "title": "B", + "properties": { + "type": { + "const": "b" + } + } + }, + "U": { + "oneOf": [ + { + "$ref": "#/$defs/A" + }, + { + "$ref": "#/$defs/B" + } + ] + } + }, + "properties": { + "x_value": { + "default": { + "type": "b" + }, + "$ref": "#/$defs/U" + } + } +} diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index 867c0c43f..d2aba7712 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -5590,6 +5590,382 @@ def test_main_jsonschema_enum_literal_type_alias_default(output_file: Path) -> N ) +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_union_default_object_ref(output_file: Path) -> None: + """Validate $ref object-union defaults through the type alias.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_union_default_object_ref.json", + output_path=output_file, + input_file_type=None, + assert_func=assert_file_content, + expected_file="type_alias_union_default_object_ref.py", + extra_args=[ + "--use-type-alias", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + ], + ) + + +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_model_default_object_ref(output_file: Path) -> None: + """Validate model-backed type alias defaults through the alias.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_model_default_object_ref.json", + output_path=output_file, + input_file_type=None, + assert_func=assert_file_content, + expected_file="type_alias_model_default_object_ref.py", + extra_args=[ + "--use-type-alias", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + ], + ) + + +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_chain_model_default_object_ref(output_file: Path) -> None: + """Validate recursively chained model-backed aliases for structured defaults.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_chain_model_default_object_ref.json", + output_path=output_file, + input_file_type=None, + assert_func=assert_file_content, + expected_file="type_alias_chain_model_default_object_ref.py", + extra_args=[ + "--use-type-alias", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + ], + ) + + +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_list_model_default_object_ref(output_file: Path) -> None: + """Validate list-of-model type alias defaults through the alias.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_list_model_default_object_ref.json", + output_path=output_file, + input_file_type=None, + assert_func=assert_file_content, + expected_file="type_alias_list_model_default_object_ref.py", + extra_args=[ + "--use-type-alias", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + ], + ) + + +def test_main_jsonschema_type_alias_inline_union_default_object(output_file: Path) -> None: + """Validate inline object-union defaults through the union type.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_inline_union_default_object.json", + output_path=output_file, + input_file_type=None, + assert_func=assert_file_content, + expected_file="type_alias_inline_union_default_object.py", + extra_args=[ + "--use-type-alias", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + ], + ) + + +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_union_default_object_ref_any_of_relevant_flags( + output_file: Path, +) -> None: + """Validate anyOf alias union defaults with the relevant type-alias flags.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_union_default_object_ref_any_of.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="type_alias_union_default_object_ref_any_of.py", + extra_args=[ + "--use-type-alias", + "--use-annotated", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-pydantic-version", + "2.11", + ], + ) + + +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_chain_union_default_object_ref_relevant_flags( + output_file: Path, +) -> None: + """Validate chained alias union defaults with the relevant type-alias flags.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_chain_union_default_object_ref.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="type_alias_chain_union_default_object_ref.py", + extra_args=[ + "--use-type-alias", + "--use-annotated", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-pydantic-version", + "2.11", + ], + ) + + +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_union_default_object_ref_one_of_relevant_flags( + output_file: Path, +) -> None: + """Validate oneOf alias union defaults with the relevant type-alias flags.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_union_default_object_ref_one_of.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="type_alias_union_default_object_ref_one_of.py", + extra_args=[ + "--use-type-alias", + "--use-annotated", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-pydantic-version", + "2.11", + ], + ) + + +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_inline_union_default_object_one_of_relevant_flags( + output_file: Path, +) -> None: + """Validate oneOf inline union defaults with the relevant type-alias flags.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_inline_union_default_object_one_of.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="type_alias_inline_union_default_object_one_of.py", + extra_args=[ + "--use-type-alias", + "--use-annotated", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-pydantic-version", + "2.11", + ], + ) + + +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_inline_union_default_object_silent_wrong_branch_relevant_flags( + output_file: Path, +) -> None: + """Validate inline unions that would silently pick the wrong branch.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_inline_union_default_object_silent_wrong_branch.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="type_alias_inline_union_default_object_silent_wrong_branch.py", + extra_args=[ + "--use-type-alias", + "--use-annotated", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-pydantic-version", + "2.11", + ], + ) + + +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_inline_list_union_default_object_relevant_flags( + output_file: Path, +) -> None: + """Validate list-of-union defaults with the relevant type-alias flags.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_inline_list_union_default_object.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="type_alias_inline_list_union_default_object.py", + extra_args=[ + "--use-type-alias", + "--use-annotated", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-pydantic-version", + "2.11", + ], + ) + + +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_list_union_default_object_ref_relevant_flags( + output_file: Path, +) -> None: + """Validate aliased list-of-union defaults with the relevant type-alias flags.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_list_union_default_object_ref.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="type_alias_list_union_default_object_ref.py", + extra_args=[ + "--use-type-alias", + "--use-annotated", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-pydantic-version", + "2.11", + ], + ) + + +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_inline_dict_union_default_object_relevant_flags( + output_file: Path, +) -> None: + """Validate dict-of-union defaults with the relevant type-alias flags.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_inline_dict_union_default_object.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="type_alias_inline_dict_union_default_object.py", + extra_args=[ + "--use-type-alias", + "--use-annotated", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-pydantic-version", + "2.11", + ], + ) + + +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_dict_union_default_object_ref_relevant_flags( + output_file: Path, +) -> None: + """Validate aliased dict-of-union defaults with the relevant type-alias flags.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_dict_union_default_object_ref.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="type_alias_dict_union_default_object_ref.py", + extra_args=[ + "--use-type-alias", + "--use-annotated", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-pydantic-version", + "2.11", + ], + ) + + +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_union_default_object_ref_mixed_scalar_relevant_flags( + output_file: Path, +) -> None: + """Validate aliased mixed scalar/model union defaults.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_union_default_object_ref_mixed_scalar.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="type_alias_union_default_object_ref_mixed_scalar.py", + extra_args=[ + "--use-type-alias", + "--use-annotated", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-pydantic-version", + "2.11", + ], + ) + + @pytest.mark.cli_doc( options=["--type-mappings"], option_description="""Override default type mappings for schema formats. From f9fe0dfeb1cd35845665572966a329aac9bb7bdd Mon Sep 17 00:00:00 2001 From: Keyan Zhang <2268452+keyz@users.noreply.github.com> Date: Sun, 8 Mar 2026 00:45:47 -0800 Subject: [PATCH 02/13] one more fixture --- .../type_alias_recursive_default_list.py | 13 ++++++++++ .../type_alias_recursive_default_list.json | 26 +++++++++++++++++++ tests/main/jsonschema/test_main_jsonschema.py | 22 ++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 tests/data/expected/main/jsonschema/type_alias_recursive_default_list.py create mode 100644 tests/data/jsonschema/type_alias_recursive_default_list.json diff --git a/tests/data/expected/main/jsonschema/type_alias_recursive_default_list.py b/tests/data/expected/main/jsonschema/type_alias_recursive_default_list.py new file mode 100644 index 000000000..e55bd9c66 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_recursive_default_list.py @@ -0,0 +1,13 @@ +# generated by datamodel-codegen: +# filename: type_alias_recursive_default_list.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from pydantic import BaseModel + +type Alias = list[Alias] | int + + +class Model(BaseModel): + x: Alias | None = [] diff --git a/tests/data/jsonschema/type_alias_recursive_default_list.json b/tests/data/jsonschema/type_alias_recursive_default_list.json new file mode 100644 index 000000000..aa97d65e8 --- /dev/null +++ b/tests/data/jsonschema/type_alias_recursive_default_list.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Alias": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/Alias" + } + }, + { + "type": "integer" + } + ] + } + }, + "title": "Model", + "type": "object", + "properties": { + "x": { + "$ref": "#/$defs/Alias", + "default": [] + } + } +} diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index d2aba7712..00c3b674d 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -5678,6 +5678,28 @@ def test_main_jsonschema_type_alias_list_model_default_object_ref(output_file: P ) +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_recursive_default_list(output_file: Path) -> None: + """Avoid recursive alias traversal loops when checking structured defaults.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_recursive_default_list.json", + output_path=output_file, + input_file_type=None, + assert_func=assert_file_content, + expected_file="type_alias_recursive_default_list.py", + extra_args=[ + "--use-type-alias", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + ], + ) + + def test_main_jsonschema_type_alias_inline_union_default_object(output_file: Path) -> None: """Validate inline object-union defaults through the union type.""" run_main_and_assert( From ca51faaaef2bd8f92bd5bbc836c2da162bfb9126 Mon Sep 17 00:00:00 2001 From: Keyan Zhang <2268452+keyz@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:02:04 -0700 Subject: [PATCH 03/13] fix --- src/datamodel_code_generator/model/_types.py | 12 ++ src/datamodel_code_generator/model/base.py | 5 +- .../model/pydantic_base.py | 13 +- .../model/pydantic_v2/base_model.py | 1 + src/datamodel_code_generator/parser/base.py | 112 +++++++++++++++++- 5 files changed, 136 insertions(+), 7 deletions(-) diff --git a/src/datamodel_code_generator/model/_types.py b/src/datamodel_code_generator/model/_types.py index e2b96fecf..0ca60c613 100644 --- a/src/datamodel_code_generator/model/_types.py +++ b/src/datamodel_code_generator/model/_types.py @@ -16,3 +16,15 @@ class WrappedDefault: def __repr__(self) -> str: """Return type constructor representation, e.g., 'CountType(10)'.""" return f"{self.type_name}({self.value!r})" + + +@dataclass(repr=False) +class ValidatedDefault: + """Represents a structured default validated through the field type.""" + + value: Any + type_name: str + + def __repr__(self) -> str: + """Return TypeAdapter validation representation for structured defaults.""" + return f"TypeAdapter({self.type_name}).validate_python({self.value!r})" diff --git a/src/datamodel_code_generator/model/base.py b/src/datamodel_code_generator/model/base.py index 45813b636..164815338 100644 --- a/src/datamodel_code_generator/model/base.py +++ b/src/datamodel_code_generator/model/base.py @@ -25,7 +25,7 @@ IMPORT_UNION, Import, ) -from datamodel_code_generator.model._types import WrappedDefault +from datamodel_code_generator.model._types import ValidatedDefault, WrappedDefault from datamodel_code_generator.reference import Reference, _BaseModel from datamodel_code_generator.types import ( ANY, @@ -38,7 +38,7 @@ get_optional_type, ) -__all__ = ["WrappedDefault"] +__all__ = ["ValidatedDefault", "WrappedDefault"] if TYPE_CHECKING: from collections.abc import Iterator @@ -599,6 +599,7 @@ class DataModel(TemplateBase, Nullable, ABC): # noqa: PLR0904 SUPPORTS_DISCRIMINATOR: ClassVar[bool] = False SUPPORTS_FIELD_RENAMING: ClassVar[bool] = False SUPPORTS_WRAPPED_DEFAULT: ClassVar[bool] = False + SUPPORTS_VALIDATED_DEFAULT: ClassVar[bool] = False SUPPORTS_KW_ONLY: ClassVar[bool] = False has_forward_reference: bool = False diff --git a/src/datamodel_code_generator/model/pydantic_base.py b/src/datamodel_code_generator/model/pydantic_base.py index a34c95f7d..fd7e855ca 100644 --- a/src/datamodel_code_generator/model/pydantic_base.py +++ b/src/datamodel_code_generator/model/pydantic_base.py @@ -19,7 +19,7 @@ DataModel, DataModelFieldBase, ) -from datamodel_code_generator.model._types import WrappedDefault +from datamodel_code_generator.model._types import ValidatedDefault, WrappedDefault from datamodel_code_generator.model.base import UNDEFINED, repr_set_sorted from datamodel_code_generator.types import STANDARD_LIST, UnionIntFloat, chain_as_tuple @@ -27,6 +27,7 @@ # (pydantic_base -> pydantic_v2.imports -> pydantic_v2/__init__ -> pydantic_v2.base_model -> pydantic_base) IMPORT_ANYURL = Import.from_full_path("pydantic.AnyUrl") IMPORT_FIELD = Import.from_full_path("pydantic.Field") +IMPORT_TYPE_ADAPTER = Import.from_full_path("pydantic.TypeAdapter") if TYPE_CHECKING: from collections import defaultdict @@ -126,7 +127,7 @@ def _get_strict_field_constraint_value(self, constraint: str, value: Any) -> Any return int(value) def _get_default_as_pydantic_model(self) -> str | None: # noqa: PLR0911, PLR0912 - if isinstance(self.default, WrappedDefault): + if isinstance(self.default, (ValidatedDefault, WrappedDefault)): return f"lambda :{self.default!r}" if self.data_type.is_list and len(self.data_type.data_types) == 1: data_type_child = self.data_type.data_types[0] @@ -296,8 +297,12 @@ def annotated(self) -> str | None: @property def imports(self) -> tuple[Import, ...]: """Get all required imports including Field if needed.""" - if self.field: - return chain_as_tuple(super().imports, (IMPORT_FIELD,)) + field = self.field + if field: + extra_imports = [IMPORT_FIELD] + if isinstance(self.default, ValidatedDefault): + extra_imports.append(IMPORT_TYPE_ADAPTER) + return chain_as_tuple(super().imports, extra_imports) return super().imports diff --git a/src/datamodel_code_generator/model/pydantic_v2/base_model.py b/src/datamodel_code_generator/model/pydantic_v2/base_model.py index 3fb3078d3..45ae53a2d 100644 --- a/src/datamodel_code_generator/model/pydantic_v2/base_model.py +++ b/src/datamodel_code_generator/model/pydantic_v2/base_model.py @@ -223,6 +223,7 @@ class BaseModel(BaseModelBase): SUPPORTS_DISCRIMINATOR: ClassVar[bool] = True SUPPORTS_FIELD_RENAMING: ClassVar[bool] = True SUPPORTS_WRAPPED_DEFAULT: ClassVar[bool] = True + SUPPORTS_VALIDATED_DEFAULT: ClassVar[bool] = True # In Pydantic 2.11+, populate_by_name is deprecated in favor of validate_by_name + validate_by_alias # Default to V2 compatible (populate_by_name) unless target_pydantic_version is specified _CONFIG_ATTRIBUTES_V2: ClassVar[list[ConfigAttribute]] = [ diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 523d54ba6..2e4967d6f 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -74,6 +74,7 @@ ConstraintsBase, DataModel, DataModelFieldBase, + ValidatedDefault, WrappedDefault, ) from datamodel_code_generator.model.enum import Enum, Member @@ -82,7 +83,7 @@ from datamodel_code_generator.parser._graph import stable_toposort from datamodel_code_generator.parser._scc import find_circular_sccs, strongly_connected_components from datamodel_code_generator.reference import ModelResolver, ModelType, Reference -from datamodel_code_generator.types import ANY, DataType, DataTypeManager +from datamodel_code_generator.types import ANY, NONE, DataType, DataTypeManager, _remove_none_from_union from datamodel_code_generator.util import camel_to_snake if TYPE_CHECKING: @@ -399,6 +400,92 @@ def iter_models_field_data_types( yield model, field, data_type +def _unwrap_type_alias(data_type: DataType) -> DataType: + current = data_type + seen: set[int] = set() + while current.reference and isinstance(current.reference.source, TypeAliasBase): + source = current.reference.source + if id(source) in seen or not source.fields: + break + seen.add(id(source)) + current = source.fields[0].data_type + return current + + +def _contains_model_reference(data_type: DataType) -> bool: + stack = [data_type] + seen: set[int] = set() + + while stack: + resolved = _unwrap_type_alias(stack.pop()) + resolved_id = id(resolved) + if resolved_id in seen: + continue + seen.add(resolved_id) + + if resolved.reference and isinstance( + resolved.reference.source, + (pydantic_model_v2.BaseModel, pydantic_model_v2.RootModel), + ): + return True + + if resolved.dict_key: + stack.append(resolved.dict_key) + stack.extend(resolved.data_types) + + return False + + +def _uses_existing_model_factory_path(data_type: DataType) -> bool: + for candidate in data_type.data_types or (data_type,): + if candidate.reference and isinstance( + candidate.reference.source, + (pydantic_model_v2.BaseModel, pydantic_model_v2.RootModel), + ): + return True + if ( + candidate.is_list + and len(candidate.data_types) == 1 + and candidate.data_types[0].reference + and isinstance( + candidate.data_types[0].reference.source, + (pydantic_model_v2.BaseModel, pydantic_model_v2.RootModel), + ) + ): + return True + return False + + +def _default_matches_plain_container_branch(default: Any, data_type: DataType) -> bool: + return (isinstance(default, dict) and data_type.is_dict and not _contains_model_reference(data_type)) or ( + isinstance(default, list) and data_type.is_list and not _contains_model_reference(data_type) + ) + + +def _get_validated_default_type_name(data_type: DataType) -> str: + type_name = data_type.type_hint + if data_type.is_optional: + return _remove_none_from_union(type_name, use_union_operator=data_type.use_union_operator) + return type_name + + +def _needs_validated_default(default: Any, data_type: DataType) -> bool: + if not isinstance(default, (dict, list)): + return False + + resolved = _unwrap_type_alias(data_type) + if not _contains_model_reference(resolved): + return False + + if resolved.is_union: + non_none_branches = tuple(branch for branch in data_type.data_types if branch.type_hint != NONE) + if len(non_none_branches) == 1 and _uses_existing_model_factory_path(non_none_branches[0]): + return False + return not any(_default_matches_plain_container_branch(default, branch) for branch in resolved.data_types) + + return not _uses_existing_model_factory_path(data_type) + + def _alias_base_class_imports( model: DataModel, aliased_imports: dict[tuple[str | None, str], Import], @@ -2150,6 +2237,28 @@ def __wrap_root_model_default_values( type_name=type_name, ) + def __wrap_validated_default_values( + self, + models: list[DataModel], + ) -> None: + """Wrap structured defaults that must be validated through the declared field type.""" + if not self.data_model_type.SUPPORTS_VALIDATED_DEFAULT: + return + for model in models: + if isinstance(model, (Enum, self.data_model_root_type)): + continue + for model_field in model.fields: + if model_field.default is None: + continue + if isinstance(model_field.default, (Member, ValidatedDefault, WrappedDefault)): + continue + if not _needs_validated_default(model_field.default, model_field.data_type): + continue + model_field.default = ValidatedDefault( + value=model_field.default, + type_name=_get_validated_default_type_name(model_field.data_type), + ) + def __override_required_field( self, models: list[DataModel], @@ -3175,6 +3284,7 @@ def _process_single_module( # noqa: PLR0913, PLR0917 self.__collapse_root_models(models, unused_models, imports, scoped_model_resolver) self.__set_default_enum_member(models) self.__wrap_root_model_default_values(models) + self.__wrap_validated_default_values(models) self.__sort_models(models, imports, use_deferred_annotations=config.use_deferred_annotations) self.__change_field_name(models) self.__apply_discriminator_type(models, imports) From d56b7465b31a8597570e9a308d58074c41bf80f2 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 9 Mar 2026 12:18:04 +0000 Subject: [PATCH 04/13] fix: use resolved.data_types for union branch filtering in _needs_validated_default --- src/datamodel_code_generator/parser/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 2e4967d6f..c493d9546 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -478,7 +478,7 @@ def _needs_validated_default(default: Any, data_type: DataType) -> bool: return False if resolved.is_union: - non_none_branches = tuple(branch for branch in data_type.data_types if branch.type_hint != NONE) + non_none_branches = tuple(branch for branch in resolved.data_types if branch.type_hint != NONE) if len(non_none_branches) == 1 and _uses_existing_model_factory_path(non_none_branches[0]): return False return not any(_default_matches_plain_container_branch(default, branch) for branch in resolved.data_types) From ce4feeccbc15fdcc4ab39016af8dda45f9b29b3a Mon Sep 17 00:00:00 2001 From: Keyan Zhang <2268452+keyz@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:38:17 -0700 Subject: [PATCH 05/13] more fixtures --- ...line_union_default_object_type_override.py | 25 ++++++++++++++ ...ne_union_default_object_type_override.json | 33 ++++++++++++++++++ tests/main/jsonschema/test_main_jsonschema.py | 34 ++++++++++++++++++- 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_type_override.py create mode 100644 tests/data/jsonschema/type_alias_inline_union_default_object_type_override.json diff --git a/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_type_override.py b/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_type_override.py new file mode 100644 index 000000000..6f5addbd2 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_type_override.py @@ -0,0 +1,25 @@ +# generated by datamodel-codegen: +# filename: type_alias_inline_union_default_object_type_override.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated, Literal + +from my_app import AliasA +from pydantic import BaseModel, Field, TypeAdapter + + +class B(BaseModel): + type: Literal['b'] = 'b' + + +class Model(BaseModel): + x: Annotated[ + AliasA | B | None, + Field( + default_factory=lambda: TypeAdapter(AliasA | B).validate_python( + {'type': 'b'} + ) + ), + ] diff --git a/tests/data/jsonschema/type_alias_inline_union_default_object_type_override.json b/tests/data/jsonschema/type_alias_inline_union_default_object_type_override.json new file mode 100644 index 000000000..31af914b7 --- /dev/null +++ b/tests/data/jsonschema/type_alias_inline_union_default_object_type_override.json @@ -0,0 +1,33 @@ +{ + "$defs": { + "A": { + "properties": { + "type": { + "const": "a" + } + } + }, + "B": { + "properties": { + "type": { + "const": "b" + } + } + } + }, + "properties": { + "x": { + "default": { + "type": "b" + }, + "anyOf": [ + { + "$ref": "#/$defs/A" + }, + { + "$ref": "#/$defs/B" + } + ] + } + } +} diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index c0f1a4e7a..2cd5d2ee3 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -5700,7 +5700,10 @@ def test_main_jsonschema_type_alias_recursive_default_list(output_file: Path) -> ], ) - +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) def test_main_jsonschema_type_alias_inline_union_default_object(output_file: Path) -> None: """Validate inline object-union defaults through the union type.""" run_main_and_assert( @@ -5827,6 +5830,35 @@ def test_main_jsonschema_type_alias_inline_union_default_object_one_of_relevant_ ) +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_inline_union_default_object_type_override_relevant_flags( + output_file: Path, +) -> None: + """Validate TypeAdapter targets after late type overrides change field types.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_inline_union_default_object_type_override.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="type_alias_inline_union_default_object_type_override.py", + extra_args=[ + "--use-type-alias", + "--use-annotated", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-pydantic-version", + "2.11", + "--type-overrides", + '{"A": "my_app.AliasA"}', + ], + ) + + @pytest.mark.skipif( int(black.__version__.split(".")[0]) < 23, reason="Installed black doesn't support the new 'type' statement", From 1a5cb962a92ec4ebc3f1940512dbbbf8ca706143 Mon Sep 17 00:00:00 2001 From: Keyan Zhang <2268452+keyz@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:39:56 -0700 Subject: [PATCH 06/13] another fixture --- ...e_union_default_object_import_collision.py | 32 ++++++++++++++ ...union_default_object_import_collision.json | 43 +++++++++++++++++++ tests/main/jsonschema/test_main_jsonschema.py | 29 +++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_import_collision.py create mode 100644 tests/data/jsonschema/type_alias_inline_union_default_object_import_collision.json diff --git a/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_import_collision.py b/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_import_collision.py new file mode 100644 index 000000000..80eca5e3e --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_inline_union_default_object_import_collision.py @@ -0,0 +1,32 @@ +# generated by datamodel-codegen: +# filename: type_alias_inline_union_default_object_import_collision.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated, Literal + +from my_app import B +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +class B1(BaseModel): + type: Literal['b'] = 'b' + + +class Other(BaseModel): + a: B | None = None + + +class Model(BaseModel): + x: Annotated[ + A | B1 | None, + Field( + default_factory=lambda: TypeAdapter(A | B1).validate_python({'type': 'b'}) + ), + ] + other: Other | None = None diff --git a/tests/data/jsonschema/type_alias_inline_union_default_object_import_collision.json b/tests/data/jsonschema/type_alias_inline_union_default_object_import_collision.json new file mode 100644 index 000000000..9aaabf1a9 --- /dev/null +++ b/tests/data/jsonschema/type_alias_inline_union_default_object_import_collision.json @@ -0,0 +1,43 @@ +{ + "$defs": { + "A": { + "properties": { + "type": { + "const": "a" + } + } + }, + "B": { + "properties": { + "type": { + "const": "b" + } + } + }, + "Other": { + "properties": { + "a": { + "$ref": "#/$defs/A" + } + } + } + }, + "properties": { + "x": { + "default": { + "type": "b" + }, + "anyOf": [ + { + "$ref": "#/$defs/A" + }, + { + "$ref": "#/$defs/B" + } + ] + }, + "other": { + "$ref": "#/$defs/Other" + } + } +} diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index 2cd5d2ee3..445e9ae20 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -5830,6 +5830,35 @@ def test_main_jsonschema_type_alias_inline_union_default_object_one_of_relevant_ ) +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_inline_union_default_object_import_collision_relevant_flags( + output_file: Path, +) -> None: + """Keep TypeAdapter targets aligned after imported-name collision renames local models.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_inline_union_default_object_import_collision.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="type_alias_inline_union_default_object_import_collision.py", + extra_args=[ + "--use-type-alias", + "--use-annotated", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-pydantic-version", + "2.11", + "--type-overrides", + '{"Other.a": "my_app.B"}', + ], + ) + + @pytest.mark.skipif( int(black.__version__.split(".")[0]) < 23, reason="Installed black doesn't support the new 'type' statement", From 7a5c516e051a6c4e86f4e3e2ed2242b954fcfc5f Mon Sep 17 00:00:00 2001 From: Keyan Zhang <2268452+keyz@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:43:04 -0700 Subject: [PATCH 07/13] use model-level capability, and defer set --- src/datamodel_code_generator/parser/base.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 231a115d5..7efb2ce23 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -412,6 +412,10 @@ def _unwrap_type_alias(data_type: DataType) -> DataType: return current +def _supports_validated_default_model(source: object) -> bool: + return isinstance(source, DataModel) and source.SUPPORTS_VALIDATED_DEFAULT and not source.is_alias + + def _contains_model_reference(data_type: DataType) -> bool: stack = [data_type] seen: set[int] = set() @@ -423,10 +427,7 @@ def _contains_model_reference(data_type: DataType) -> bool: continue seen.add(resolved_id) - if resolved.reference and isinstance( - resolved.reference.source, - (pydantic_model_v2.BaseModel, pydantic_model_v2.RootModel), - ): + if resolved.reference and _supports_validated_default_model(resolved.reference.source): return True if resolved.dict_key: @@ -438,19 +439,13 @@ def _contains_model_reference(data_type: DataType) -> bool: def _uses_existing_model_factory_path(data_type: DataType) -> bool: for candidate in data_type.data_types or (data_type,): - if candidate.reference and isinstance( - candidate.reference.source, - (pydantic_model_v2.BaseModel, pydantic_model_v2.RootModel), - ): + if candidate.reference and _supports_validated_default_model(candidate.reference.source): return True if ( candidate.is_list and len(candidate.data_types) == 1 and candidate.data_types[0].reference - and isinstance( - candidate.data_types[0].reference.source, - (pydantic_model_v2.BaseModel, pydantic_model_v2.RootModel), - ) + and _supports_validated_default_model(candidate.data_types[0].reference.source) ): return True return False @@ -3286,7 +3281,6 @@ def _process_single_module( # noqa: PLR0913, PLR0917 self.__collapse_root_models(models, unused_models, imports, scoped_model_resolver) self.__set_default_enum_member(models) self.__wrap_root_model_default_values(models) - self.__wrap_validated_default_values(models) self.__sort_models(models, imports, use_deferred_annotations=config.use_deferred_annotations) self.__change_field_name(models) self.__apply_discriminator_type(models, imports) @@ -3295,6 +3289,7 @@ def _process_single_module( # noqa: PLR0913, PLR0917 models = self.__remove_overridden_models(models) self.__apply_type_overrides(models) self.__update_type_aliases(models) + self.__wrap_validated_default_values(models) return ModuleContext(module, module_, models, is_init, imports, scoped_model_resolver) From 54b947c42179bafb9eb187e21af65d9516a1672e Mon Sep 17 00:00:00 2001 From: Keyan Zhang <2268452+keyz@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:45:54 -0700 Subject: [PATCH 08/13] fix overrides --- src/datamodel_code_generator/parser/base.py | 6 +++++- tests/main/jsonschema/test_main_jsonschema.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 7efb2ce23..e1896db22 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -2245,7 +2245,10 @@ def __wrap_validated_default_values( for model_field in model.fields: if model_field.default is None: continue - if isinstance(model_field.default, (Member, ValidatedDefault, WrappedDefault)): + if isinstance(model_field.default, (Member, WrappedDefault)): + continue + if isinstance(model_field.default, ValidatedDefault): + model_field.default.type_name = _get_validated_default_type_name(model_field.data_type) continue if not _needs_validated_default(model_field.default, model_field.data_type): continue @@ -3322,6 +3325,7 @@ def _finalize_modules( for ctx in contexts: self.__change_imported_model_name(ctx.models, ctx.imports, ctx.scoped_model_resolver) + self.__wrap_validated_default_values(ctx.models) def _generate_module_output( # noqa: PLR0913, PLR0917 self, diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index 445e9ae20..83125f872 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -5700,6 +5700,7 @@ def test_main_jsonschema_type_alias_recursive_default_list(output_file: Path) -> ], ) + @pytest.mark.skipif( int(black.__version__.split(".")[0]) < 23, reason="Installed black doesn't support the new 'type' statement", From f1f9f96c097558896c0a950b3ade5b216dba7f24 Mon Sep 17 00:00:00 2001 From: Keyan Zhang <2268452+keyz@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:11:52 -0700 Subject: [PATCH 09/13] another fix --- src/datamodel_code_generator/parser/base.py | 4 +- ..._nullable_list_model_default_object_ref.py | 22 ++++++++++ ...alias_nullable_model_default_object_ref.py | 22 ++++++++++ ...ullable_list_model_default_object_ref.json | 34 ++++++++++++++ ...ias_nullable_model_default_object_ref.json | 29 ++++++++++++ tests/main/jsonschema/test_main_jsonschema.py | 44 +++++++++++++++++++ 6 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 tests/data/expected/main/jsonschema/type_alias_nullable_list_model_default_object_ref.py create mode 100644 tests/data/expected/main/jsonschema/type_alias_nullable_model_default_object_ref.py create mode 100644 tests/data/jsonschema/type_alias_nullable_list_model_default_object_ref.json create mode 100644 tests/data/jsonschema/type_alias_nullable_model_default_object_ref.json diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index e1896db22..3750b6b73 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -474,7 +474,9 @@ def _needs_validated_default(default: Any, data_type: DataType) -> bool: if resolved.is_union: non_none_branches = tuple(branch for branch in resolved.data_types if branch.type_hint != NONE) - if len(non_none_branches) == 1 and _uses_existing_model_factory_path(non_none_branches[0]): + # This must use the declared field type, not the unwrapped branch: alias-backed + # unions like `type Alias = A | None` only get the correct factory via `Alias`. + if len(non_none_branches) == 1 and _uses_existing_model_factory_path(data_type): return False return not any(_default_matches_plain_container_branch(default, branch) for branch in resolved.data_types) diff --git a/tests/data/expected/main/jsonschema/type_alias_nullable_list_model_default_object_ref.py b/tests/data/expected/main/jsonschema/type_alias_nullable_list_model_default_object_ref.py new file mode 100644 index 000000000..5ac27b764 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_nullable_list_model_default_object_ref.py @@ -0,0 +1,22 @@ +# generated by datamodel-codegen: +# filename: type_alias_nullable_list_model_default_object_ref.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +type Alias = list[A] | None + + +class Model(BaseModel): + x: Alias | None = Field( + default_factory=lambda: TypeAdapter(Alias).validate_python([{'type': 'a'}]) + ) diff --git a/tests/data/expected/main/jsonschema/type_alias_nullable_model_default_object_ref.py b/tests/data/expected/main/jsonschema/type_alias_nullable_model_default_object_ref.py new file mode 100644 index 000000000..b2d51ed47 --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_nullable_model_default_object_ref.py @@ -0,0 +1,22 @@ +# generated by datamodel-codegen: +# filename: type_alias_nullable_model_default_object_ref.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, Field, TypeAdapter + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +type Alias = A | None + + +class Model(BaseModel): + x: Alias | None = Field( + default_factory=lambda: TypeAdapter(Alias).validate_python({'type': 'a'}) + ) diff --git a/tests/data/jsonschema/type_alias_nullable_list_model_default_object_ref.json b/tests/data/jsonschema/type_alias_nullable_list_model_default_object_ref.json new file mode 100644 index 000000000..35baa4002 --- /dev/null +++ b/tests/data/jsonschema/type_alias_nullable_list_model_default_object_ref.json @@ -0,0 +1,34 @@ +{ + "$defs": { + "A": { + "properties": { + "type": { + "const": "a" + } + } + }, + "Alias": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/A" + } + }, + { + "type": "null" + } + ] + } + }, + "properties": { + "x": { + "default": [ + { + "type": "a" + } + ], + "$ref": "#/$defs/Alias" + } + } +} diff --git a/tests/data/jsonschema/type_alias_nullable_model_default_object_ref.json b/tests/data/jsonschema/type_alias_nullable_model_default_object_ref.json new file mode 100644 index 000000000..f4d30ba5d --- /dev/null +++ b/tests/data/jsonschema/type_alias_nullable_model_default_object_ref.json @@ -0,0 +1,29 @@ +{ + "$defs": { + "A": { + "properties": { + "type": { + "const": "a" + } + } + }, + "Alias": { + "anyOf": [ + { + "$ref": "#/$defs/A" + }, + { + "type": "null" + } + ] + } + }, + "properties": { + "x": { + "default": { + "type": "a" + }, + "$ref": "#/$defs/Alias" + } + } +} diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index 83125f872..b880ff93c 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -5679,6 +5679,50 @@ def test_main_jsonschema_type_alias_list_model_default_object_ref(output_file: P ) +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_nullable_model_default_object_ref(output_file: Path) -> None: + """Validate nullable model-backed type alias defaults through the alias.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_nullable_model_default_object_ref.json", + output_path=output_file, + input_file_type=None, + assert_func=assert_file_content, + expected_file="type_alias_nullable_model_default_object_ref.py", + extra_args=[ + "--use-type-alias", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + ], + ) + + +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_nullable_list_model_default_object_ref(output_file: Path) -> None: + """Validate nullable list-of-model type alias defaults through the alias.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_nullable_list_model_default_object_ref.json", + output_path=output_file, + input_file_type=None, + assert_func=assert_file_content, + expected_file="type_alias_nullable_list_model_default_object_ref.py", + extra_args=[ + "--use-type-alias", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + ], + ) + + @pytest.mark.skipif( int(black.__version__.split(".")[0]) < 23, reason="Installed black doesn't support the new 'type' statement", From 31833bfc46a0d26960af3a0ea4cb2ab957cb66a1 Mon Sep 17 00:00:00 2001 From: Keyan Zhang <2268452+keyz@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:19:16 -0700 Subject: [PATCH 10/13] one more --- src/datamodel_code_generator/parser/base.py | 5 +-- ...on_default_object_ref_dict_alias_branch.py | 23 ++++++++++++ ..._default_object_ref_dict_alias_branch.json | 35 +++++++++++++++++++ tests/main/jsonschema/test_main_jsonschema.py | 27 ++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_dict_alias_branch.py create mode 100644 tests/data/jsonschema/type_alias_union_default_object_ref_dict_alias_branch.json diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 3750b6b73..971159722 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -452,8 +452,9 @@ def _uses_existing_model_factory_path(data_type: DataType) -> bool: def _default_matches_plain_container_branch(default: Any, data_type: DataType) -> bool: - return (isinstance(default, dict) and data_type.is_dict and not _contains_model_reference(data_type)) or ( - isinstance(default, list) and data_type.is_list and not _contains_model_reference(data_type) + resolved = _unwrap_type_alias(data_type) + return (isinstance(default, dict) and resolved.is_dict and not _contains_model_reference(resolved)) or ( + isinstance(default, list) and resolved.is_list and not _contains_model_reference(resolved) ) diff --git a/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_dict_alias_branch.py b/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_dict_alias_branch.py new file mode 100644 index 000000000..38faef29d --- /dev/null +++ b/tests/data/expected/main/jsonschema/type_alias_union_default_object_ref_dict_alias_branch.py @@ -0,0 +1,23 @@ +# generated by datamodel-codegen: +# filename: type_alias_union_default_object_ref_dict_alias_branch.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel + + +class A(BaseModel): + type: Literal['a'] = 'a' + + +type AliasDict = dict[str, str] + + +type U = AliasDict | A + + +class Model(BaseModel): + x: U | None = {'type': 'a'} diff --git a/tests/data/jsonschema/type_alias_union_default_object_ref_dict_alias_branch.json b/tests/data/jsonschema/type_alias_union_default_object_ref_dict_alias_branch.json new file mode 100644 index 000000000..20fd47163 --- /dev/null +++ b/tests/data/jsonschema/type_alias_union_default_object_ref_dict_alias_branch.json @@ -0,0 +1,35 @@ +{ + "$defs": { + "A": { + "properties": { + "type": { + "const": "a" + } + } + }, + "AliasDict": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "U": { + "anyOf": [ + { + "$ref": "#/$defs/AliasDict" + }, + { + "$ref": "#/$defs/A" + } + ] + } + }, + "properties": { + "x": { + "default": { + "type": "a" + }, + "$ref": "#/$defs/U" + } + } +} diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index b880ff93c..dd71d3907 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -6068,6 +6068,33 @@ def test_main_jsonschema_type_alias_dict_union_default_object_ref_relevant_flags ) +@pytest.mark.skipif( + int(black.__version__.split(".")[0]) < 23, + reason="Installed black doesn't support the new 'type' statement", +) +def test_main_jsonschema_type_alias_union_default_object_ref_dict_alias_branch_relevant_flags( + output_file: Path, +) -> None: + """Keep raw defaults when a plain dict union branch is hidden behind a type alias.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "type_alias_union_default_object_ref_dict_alias_branch.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="type_alias_union_default_object_ref_dict_alias_branch.py", + extra_args=[ + "--use-type-alias", + "--use-annotated", + "--target-python-version", + "3.12", + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-pydantic-version", + "2.11", + ], + ) + + @pytest.mark.skipif( int(black.__version__.split(".")[0]) < 23, reason="Installed black doesn't support the new 'type' statement", From 612175033b142029ab018a4aeb5275805b7d8ef9 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 10 Mar 2026 05:08:41 +0000 Subject: [PATCH 11/13] test: stub my_app for py312 exec validation --- tests/main/jsonschema/test_main_jsonschema.py | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index dd71d3907..5dd616934 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -25,7 +25,7 @@ from datamodel_code_generator.__main__ import Exit, main from datamodel_code_generator.format import is_supported_in_black from datamodel_code_generator.model import base as model_base -from tests.conftest import assert_directory_content, freeze_time +from tests.conftest import assert_directory_content, freeze_time, validate_generated_code from tests.main.conftest import ( ALIASES_DATA_PATH, BLACK_PY313_SKIP, @@ -48,6 +48,27 @@ FixtureRequest = pytest.FixtureRequest +def _install_test_my_app(base_dir: Path, monkeypatch: pytest.MonkeyPatch) -> None: + package_dir = base_dir / "my_app" + package_dir.mkdir() + (package_dir / "__init__.py").write_text( + """from typing import Literal + +from pydantic import BaseModel + + +class AliasA(BaseModel): + type: Literal["a"] = "a" + + +class B(BaseModel): + type: Literal["b"] = "b" +""", + encoding="utf-8", + ) + monkeypatch.syspath_prepend(str(base_dir)) + + @pytest.mark.benchmark def test_main_inheritance_forward_ref(output_file: Path, tmp_path: Path) -> None: """Test inheritance with forward references.""" @@ -5881,6 +5902,7 @@ def test_main_jsonschema_type_alias_inline_union_default_object_one_of_relevant_ ) def test_main_jsonschema_type_alias_inline_union_default_object_import_collision_relevant_flags( output_file: Path, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Keep TypeAdapter targets aligned after imported-name collision renames local models.""" run_main_and_assert( @@ -5901,7 +5923,10 @@ def test_main_jsonschema_type_alias_inline_union_default_object_import_collision "--type-overrides", '{"Other.a": "my_app.B"}', ], + skip_code_validation=True, ) + _install_test_my_app(output_file.parent, monkeypatch) + validate_generated_code(output_file.read_text(encoding="utf-8"), str(output_file), do_exec=True) @pytest.mark.skipif( @@ -5910,6 +5935,7 @@ def test_main_jsonschema_type_alias_inline_union_default_object_import_collision ) def test_main_jsonschema_type_alias_inline_union_default_object_type_override_relevant_flags( output_file: Path, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Validate TypeAdapter targets after late type overrides change field types.""" run_main_and_assert( @@ -5930,7 +5956,10 @@ def test_main_jsonschema_type_alias_inline_union_default_object_type_override_re "--type-overrides", '{"A": "my_app.AliasA"}', ], + skip_code_validation=True, ) + _install_test_my_app(output_file.parent, monkeypatch) + validate_generated_code(output_file.read_text(encoding="utf-8"), str(output_file), do_exec=True) @pytest.mark.skipif( From c318230e33426f5132253d368d62f34d26328dea Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 10 Mar 2026 08:44:09 +0000 Subject: [PATCH 12/13] test: cover validated default parser helpers --- tests/parser/test_base.py | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/parser/test_base.py b/tests/parser/test_base.py index 5c8e98b73..34c70f7ed 100644 --- a/tests/parser/test_base.py +++ b/tests/parser/test_base.py @@ -17,6 +17,10 @@ from datamodel_code_generator.model.type_alias import TypeAlias, TypeAliasTypeBackport, TypeStatement from datamodel_code_generator.parser.base import ( Parser, + _contains_model_reference, + _get_validated_default_type_name, + _needs_validated_default, + _unwrap_type_alias, add_model_path_to_list, escape_characters, exact_import, @@ -646,3 +650,51 @@ def test_build_module_dependency_graph_with_missing_ref(parser_fixture: C) -> No graph = parser_fixture._Parser__build_module_dependency_graph(module_models_list) assert graph == {("pkg",): set()} + + +def test_unwrap_type_alias_stops_on_recursive_alias() -> None: + """Test _unwrap_type_alias stops when a type alias resolves back to itself.""" + alias_reference = Reference(path="Alias", original_name="Alias", name="Alias") + alias_data_type = DataType(reference=alias_reference) + alias_model = TypeStatement(fields=[DataModelField(data_type=alias_data_type)], reference=alias_reference) + + assert alias_reference.source is alias_model + assert _unwrap_type_alias(alias_data_type) is alias_data_type + + +def test_contains_model_reference_traverses_nested_data_types() -> None: + """Test _contains_model_reference walks nested container data types.""" + model_reference = Reference(path="Item", original_name="Item", name="Item") + BaseModel(fields=[], reference=model_reference) + + list_of_models = DataType( + is_list=True, + data_types=[DataType(reference=model_reference)], + use_union_operator=True, + ) + + assert _contains_model_reference(list_of_models) is True + + +def test_get_validated_default_type_name_strips_optional_none() -> None: + """Test _get_validated_default_type_name removes None from optional types.""" + optional_string = DataType(type="str", is_optional=True, use_union_operator=True) + + assert _get_validated_default_type_name(optional_string) == "str" + + +def test_needs_validated_default_for_union_type_alias() -> None: + """Test _needs_validated_default resolves type aliases before checking union branches.""" + a_reference = Reference(path="A", original_name="A", name="A") + b_reference = Reference(path="B", original_name="B", name="B") + BaseModel(fields=[], reference=a_reference) + BaseModel(fields=[], reference=b_reference) + + union_data_type = DataType( + data_types=[DataType(reference=a_reference), DataType(reference=b_reference)], + use_union_operator=True, + ) + alias_reference = Reference(path="Alias", original_name="Alias", name="Alias") + TypeStatement(fields=[DataModelField(data_type=union_data_type)], reference=alias_reference) + + assert _needs_validated_default({"type": "b"}, DataType(reference=alias_reference)) is True From 689c7d60a981d3c8df4145b44988b7992e2b344d Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 10 Mar 2026 08:57:47 +0000 Subject: [PATCH 13/13] test: cover remaining validated default branches --- tests/parser/test_base.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/parser/test_base.py b/tests/parser/test_base.py index 34c70f7ed..1284c8875 100644 --- a/tests/parser/test_base.py +++ b/tests/parser/test_base.py @@ -676,6 +676,20 @@ def test_contains_model_reference_traverses_nested_data_types() -> None: assert _contains_model_reference(list_of_models) is True +def test_contains_model_reference_traverses_dict_key() -> None: + """Test _contains_model_reference walks dict_key data types.""" + model_reference = Reference(path="KeyModel", original_name="KeyModel", name="KeyModel") + BaseModel(fields=[], reference=model_reference) + + dict_with_model_key = DataType( + is_dict=True, + dict_key=DataType(reference=model_reference), + use_union_operator=True, + ) + + assert _contains_model_reference(dict_with_model_key) is True + + def test_get_validated_default_type_name_strips_optional_none() -> None: """Test _get_validated_default_type_name removes None from optional types.""" optional_string = DataType(type="str", is_optional=True, use_union_operator=True) @@ -698,3 +712,16 @@ def test_needs_validated_default_for_union_type_alias() -> None: TypeStatement(fields=[DataModelField(data_type=union_data_type)], reference=alias_reference) assert _needs_validated_default({"type": "b"}, DataType(reference=alias_reference)) is True + + +def test_needs_validated_default_skips_optional_single_model_union() -> None: + """Test _needs_validated_default keeps the existing factory path for A | None.""" + model_reference = Reference(path="A", original_name="A", name="A") + BaseModel(fields=[], reference=model_reference) + + optional_model_union = DataType( + data_types=[DataType(reference=model_reference), DataType(type="None")], + use_union_operator=True, + ) + + assert _needs_validated_default({"type": "a"}, optional_model_union) is False