From e7fe3592ab49a9a8a2e45f86391f98dd7a6633f0 Mon Sep 17 00:00:00 2001 From: Ryan McGinty Date: Tue, 6 Jan 2026 13:01:01 -0800 Subject: [PATCH 1/6] adding tests to highlight the graphql bug --- .../main/graphql/empty_list_default.py | 17 ++++++++++ .../graphql/pydantic_v2_empty_list_default.py | 17 ++++++++++ tests/data/graphql/empty_list_default.graphql | 10 ++++++ tests/main/graphql/test_main_graphql.py | 33 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 tests/data/expected/main/graphql/empty_list_default.py create mode 100644 tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py create mode 100644 tests/data/graphql/empty_list_default.graphql diff --git a/tests/data/expected/main/graphql/empty_list_default.py b/tests/data/expected/main/graphql/empty_list_default.py new file mode 100644 index 000000000..564d91ab9 --- /dev/null +++ b/tests/data/expected/main/graphql/empty_list_default.py @@ -0,0 +1,17 @@ +# generated by datamodel-codegen: +# filename: empty_list_default.graphql +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class Container(BaseModel): + name: Optional[str] = None + + +class PodSpec(BaseModel): + containers: Optional[List[Container]] = Field(default_factory=list) diff --git a/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py b/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py new file mode 100644 index 000000000..564d91ab9 --- /dev/null +++ b/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py @@ -0,0 +1,17 @@ +# generated by datamodel-codegen: +# filename: empty_list_default.graphql +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class Container(BaseModel): + name: Optional[str] = None + + +class PodSpec(BaseModel): + containers: Optional[List[Container]] = Field(default_factory=list) diff --git a/tests/data/graphql/empty_list_default.graphql b/tests/data/graphql/empty_list_default.graphql new file mode 100644 index 000000000..407b94ea9 --- /dev/null +++ b/tests/data/graphql/empty_list_default.graphql @@ -0,0 +1,10 @@ +""" +Equivalent types for the GraphQL schemas. +""" +input Container { + name: String +} + +input PodSpec { + containers: [Container!]! = [] +} diff --git a/tests/main/graphql/test_main_graphql.py b/tests/main/graphql/test_main_graphql.py index 14b2ae52a..18de8c220 100644 --- a/tests/main/graphql/test_main_graphql.py +++ b/tests/main/graphql/test_main_graphql.py @@ -104,6 +104,39 @@ def test_main_use_default_kwarg(output_file: Path) -> None: ) +@pytest.mark.parametrize( + ("output_model", "expected_output"), + [ + ( + "pydantic.BaseModel", + "empty_list_default.py", + ), + ( + "pydantic_v2.BaseModel", + "pydantic_v2_empty_list_default.py", + ), + ], +) +@pytest.mark.skipif( + black.__version__.split(".")[0] == "19", + reason="Installed black doesn't support the old style", +) +def test_main_graphql_empty_list_default(output_model: str, expected_output: str, output_file: Path) -> None: + """Test GraphQL generation with empty list default values.""" + run_main_and_assert( + input_path=GRAPHQL_DATA_PATH / "empty_list_default.graphql", + output_path=output_file, + expected_file=GRAPHQL_DATA_PATH / expected_output, + input_file_type="graphql", + extra_args=[ + "--output-model-type", + output_model, + "--target-python-version", + "3.13", + ], + ) + + @pytest.mark.skipif( black.__version__.split(".")[0] == "19", reason="Installed black doesn't support the old style", From 0c5c6dda393235243ece1486af145548660fdc1e Mon Sep 17 00:00:00 2001 From: Ryan McGinty Date: Tue, 6 Jan 2026 19:24:25 -0800 Subject: [PATCH 2/6] fixing test to actually compare files (previously was not) --- .../expected/main/graphql/empty_list_default.py | 10 +++++++--- .../main/graphql/pydantic_v2_empty_list_default.py | 10 +++++++--- tests/data/graphql/empty_list_default.graphql | 10 +++++----- tests/main/graphql/test_main_graphql.py | 13 ++++++++++--- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/tests/data/expected/main/graphql/empty_list_default.py b/tests/data/expected/main/graphql/empty_list_default.py index 564d91ab9..cfd851cb8 100644 --- a/tests/data/expected/main/graphql/empty_list_default.py +++ b/tests/data/expected/main/graphql/empty_list_default.py @@ -4,14 +4,18 @@ from __future__ import annotations -from typing import List, Optional +from typing import Literal from pydantic import BaseModel, Field class Container(BaseModel): - name: Optional[str] = None + name: str + typename__: Literal['Container'] | None = Field('Container', alias='__typename') class PodSpec(BaseModel): - containers: Optional[List[Container]] = Field(default_factory=list) + container_list: list[Container] = Field(default_factory=list) + container_list_or_none: list[Container | None] = Field(default_factory=list) + container_or_none_list_or_none: list[Container | None] | None = Field(default_factory=list) + typename__: Literal['PodSpec'] | None = Field('PodSpec', alias='__typename') \ No newline at end of file diff --git a/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py b/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py index 564d91ab9..cfd851cb8 100644 --- a/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py +++ b/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py @@ -4,14 +4,18 @@ from __future__ import annotations -from typing import List, Optional +from typing import Literal from pydantic import BaseModel, Field class Container(BaseModel): - name: Optional[str] = None + name: str + typename__: Literal['Container'] | None = Field('Container', alias='__typename') class PodSpec(BaseModel): - containers: Optional[List[Container]] = Field(default_factory=list) + container_list: list[Container] = Field(default_factory=list) + container_list_or_none: list[Container | None] = Field(default_factory=list) + container_or_none_list_or_none: list[Container | None] | None = Field(default_factory=list) + typename__: Literal['PodSpec'] | None = Field('PodSpec', alias='__typename') \ No newline at end of file diff --git a/tests/data/graphql/empty_list_default.graphql b/tests/data/graphql/empty_list_default.graphql index 407b94ea9..c561d3dc4 100644 --- a/tests/data/graphql/empty_list_default.graphql +++ b/tests/data/graphql/empty_list_default.graphql @@ -1,10 +1,10 @@ -""" -Equivalent types for the GraphQL schemas. -""" + input Container { - name: String + name: String! } input PodSpec { - containers: [Container!]! = [] + container_list: [Container!]! = [] + container_list_or_none: [Container]! = [] + container_or_none_list_or_none: [Container] = [] } diff --git a/tests/main/graphql/test_main_graphql.py b/tests/main/graphql/test_main_graphql.py index 18de8c220..762260240 100644 --- a/tests/main/graphql/test_main_graphql.py +++ b/tests/main/graphql/test_main_graphql.py @@ -7,7 +7,13 @@ import black import pytest -from tests.main.conftest import DEFAULT_VALUES_DATA_PATH, GRAPHQL_DATA_PATH, LEGACY_BLACK_SKIP, run_main_and_assert +from tests.main.conftest import ( + DEFAULT_VALUES_DATA_PATH, + EXPECTED_GRAPHQL_PATH, + GRAPHQL_DATA_PATH, + LEGACY_BLACK_SKIP, + run_main_and_assert +) from tests.main.graphql.conftest import assert_file_content if TYPE_CHECKING: @@ -126,13 +132,14 @@ def test_main_graphql_empty_list_default(output_model: str, expected_output: str run_main_and_assert( input_path=GRAPHQL_DATA_PATH / "empty_list_default.graphql", output_path=output_file, - expected_file=GRAPHQL_DATA_PATH / expected_output, + assert_func=assert_file_content, + expected_file=EXPECTED_GRAPHQL_PATH / expected_output, input_file_type="graphql", extra_args=[ "--output-model-type", output_model, "--target-python-version", - "3.13", + "3.12", ], ) From 90adc0154409ee1d35e654988b9d039ebaf34ed2 Mon Sep 17 00:00:00 2001 From: Ryan McGinty Date: Tue, 6 Jan 2026 19:27:24 -0800 Subject: [PATCH 3/6] fix previous test that did not specify assert_func --- tests/main/openapi/test_main_openapi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/main/openapi/test_main_openapi.py b/tests/main/openapi/test_main_openapi.py index 495c2309b..0c8decff1 100644 --- a/tests/main/openapi/test_main_openapi.py +++ b/tests/main/openapi/test_main_openapi.py @@ -2808,6 +2808,7 @@ def test_main_openapi_empty_list_default(output_model: str, expected_output: str input_path=OPEN_API_DATA_PATH / "empty_list_default.yaml", output_path=output_file, expected_file=EXPECTED_OPENAPI_PATH / expected_output, + assert_func=assert_file_content, input_file_type="openapi", extra_args=[ "--output-model-type", From 912af8cdcacec6234072328056bd16a6cb92163f Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sat, 10 Jan 2026 07:14:10 +0000 Subject: [PATCH 4/6] Fix empty list default for GraphQL list fields --- .../model/pydantic/base_model.py | 16 +++++++++++++++- tests/main/graphql/test_main_graphql.py | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/datamodel_code_generator/model/pydantic/base_model.py b/src/datamodel_code_generator/model/pydantic/base_model.py index 6368a9379..54ab4303e 100644 --- a/src/datamodel_code_generator/model/pydantic/base_model.py +++ b/src/datamodel_code_generator/model/pydantic/base_model.py @@ -126,9 +126,23 @@ def _get_strict_field_constraint_value(self, constraint: str, value: Any) -> Any return value return int(value) - def _get_default_as_pydantic_model(self) -> str | None: + def _get_default_as_pydantic_model(self) -> str | None: # noqa: PLR0911, PLR0912 if isinstance(self.default, WrappedDefault): return f"lambda :{self.default!r}" + # Handle the case where self.data_type.is_list is True directly (e.g., GraphQL) + if self.data_type.is_list and len(self.data_type.data_types) == 1: + data_type_child = self.data_type.data_types[0] + if ( + data_type_child.reference + and isinstance(data_type_child.reference.source, BaseModelBase) + and isinstance(self.default, list) + ): + if not self.default: + return STANDARD_LIST + return ( # pragma: no cover + f"lambda :[{data_type_child.alias or data_type_child.reference.source.class_name}." + f"{self._PARSE_METHOD}(v) for v in {self.default!r}]" + ) for data_type in self.data_type.data_types or (self.data_type,): # TODO: Check nested data_types if data_type.is_dict: diff --git a/tests/main/graphql/test_main_graphql.py b/tests/main/graphql/test_main_graphql.py index 762260240..13e5921b7 100644 --- a/tests/main/graphql/test_main_graphql.py +++ b/tests/main/graphql/test_main_graphql.py @@ -12,7 +12,7 @@ EXPECTED_GRAPHQL_PATH, GRAPHQL_DATA_PATH, LEGACY_BLACK_SKIP, - run_main_and_assert + run_main_and_assert, ) from tests.main.graphql.conftest import assert_file_content From 8431860d3263cf9d78d4b07f6bf7fc334e09e298 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 12 Jan 2026 17:41:27 +0000 Subject: [PATCH 5/6] Fix GraphQL empty list default handling --- .../model/pydantic/base_model.py | 5 ++--- .../main/graphql/empty_list_default.py | 19 ++++++++++++++++--- .../graphql/pydantic_v2_empty_list_default.py | 19 ++++++++++++++++--- .../main/openapi/empty_list_default.py | 6 ++---- .../openapi/pydantic_v2_empty_list_default.py | 6 ++---- 5 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/datamodel_code_generator/model/pydantic/base_model.py b/src/datamodel_code_generator/model/pydantic/base_model.py index 54ab4303e..ce83fad6d 100644 --- a/src/datamodel_code_generator/model/pydantic/base_model.py +++ b/src/datamodel_code_generator/model/pydantic/base_model.py @@ -129,7 +129,6 @@ def _get_strict_field_constraint_value(self, constraint: str, value: Any) -> Any def _get_default_as_pydantic_model(self) -> str | None: # noqa: PLR0911, PLR0912 if isinstance(self.default, WrappedDefault): return f"lambda :{self.default!r}" - # Handle the case where self.data_type.is_list is True directly (e.g., GraphQL) if self.data_type.is_list and len(self.data_type.data_types) == 1: data_type_child = self.data_type.data_types[0] if ( @@ -234,7 +233,7 @@ def __str__(self) -> str: # noqa: PLR0912 elif isinstance(discriminator, dict): # pragma: no cover data["discriminator"] = discriminator["propertyName"] - if self.required: + if self.required and not self.has_default: default_factory = None elif self.default is not UNDEFINED and self.default is not None and "default_factory" not in data: default_factory = self._get_default_as_pydantic_model() @@ -263,7 +262,7 @@ def __str__(self) -> str: # noqa: PLR0912 if self.use_annotated: field_arguments = self._process_annotated_field_arguments(field_arguments) - elif self.required: + elif self.required and not default_factory: field_arguments = ["...", *field_arguments] elif not default_factory: default_repr = repr_set_sorted(self.default) if isinstance(self.default, set) else repr(self.default) diff --git a/tests/data/expected/main/graphql/empty_list_default.py b/tests/data/expected/main/graphql/empty_list_default.py index cfd851cb8..8def8a752 100644 --- a/tests/data/expected/main/graphql/empty_list_default.py +++ b/tests/data/expected/main/graphql/empty_list_default.py @@ -8,14 +8,27 @@ from pydantic import BaseModel, Field +type Boolean = bool +""" +The `Boolean` scalar type represents `true` or `false`. +""" + + +type String = str +""" +The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. +""" + class Container(BaseModel): - name: str + name: String typename__: Literal['Container'] | None = Field('Container', alias='__typename') class PodSpec(BaseModel): container_list: list[Container] = Field(default_factory=list) container_list_or_none: list[Container | None] = Field(default_factory=list) - container_or_none_list_or_none: list[Container | None] | None = Field(default_factory=list) - typename__: Literal['PodSpec'] | None = Field('PodSpec', alias='__typename') \ No newline at end of file + container_or_none_list_or_none: list[Container | None] | None = Field( + default_factory=list + ) + typename__: Literal['PodSpec'] | None = Field('PodSpec', alias='__typename') diff --git a/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py b/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py index cfd851cb8..8def8a752 100644 --- a/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py +++ b/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py @@ -8,14 +8,27 @@ from pydantic import BaseModel, Field +type Boolean = bool +""" +The `Boolean` scalar type represents `true` or `false`. +""" + + +type String = str +""" +The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. +""" + class Container(BaseModel): - name: str + name: String typename__: Literal['Container'] | None = Field('Container', alias='__typename') class PodSpec(BaseModel): container_list: list[Container] = Field(default_factory=list) container_list_or_none: list[Container | None] = Field(default_factory=list) - container_or_none_list_or_none: list[Container | None] | None = Field(default_factory=list) - typename__: Literal['PodSpec'] | None = Field('PodSpec', alias='__typename') \ No newline at end of file + container_or_none_list_or_none: list[Container | None] | None = Field( + default_factory=list + ) + typename__: Literal['PodSpec'] | None = Field('PodSpec', alias='__typename') diff --git a/tests/data/expected/main/openapi/empty_list_default.py b/tests/data/expected/main/openapi/empty_list_default.py index 34ef0c865..4678aeb8d 100644 --- a/tests/data/expected/main/openapi/empty_list_default.py +++ b/tests/data/expected/main/openapi/empty_list_default.py @@ -4,14 +4,12 @@ from __future__ import annotations -from typing import List, Optional - from pydantic import BaseModel, Field class Container(BaseModel): - name: Optional[str] = None + name: str | None = None class PodSpec(BaseModel): - containers: Optional[List[Container]] = Field(default_factory=list) + containers: list[Container] | None = Field(default_factory=list) diff --git a/tests/data/expected/main/openapi/pydantic_v2_empty_list_default.py b/tests/data/expected/main/openapi/pydantic_v2_empty_list_default.py index 34ef0c865..4678aeb8d 100644 --- a/tests/data/expected/main/openapi/pydantic_v2_empty_list_default.py +++ b/tests/data/expected/main/openapi/pydantic_v2_empty_list_default.py @@ -4,14 +4,12 @@ from __future__ import annotations -from typing import List, Optional - from pydantic import BaseModel, Field class Container(BaseModel): - name: Optional[str] = None + name: str | None = None class PodSpec(BaseModel): - containers: Optional[List[Container]] = Field(default_factory=list) + containers: list[Container] | None = Field(default_factory=list) From df9dd1e9f5198fcf048825b217120fdb647dc25a Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 12 Jan 2026 17:46:09 +0000 Subject: [PATCH 6/6] Skip empty list default tests on black 22 --- tests/main/graphql/test_main_graphql.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/main/graphql/test_main_graphql.py b/tests/main/graphql/test_main_graphql.py index 13e5921b7..61fdc7846 100644 --- a/tests/main/graphql/test_main_graphql.py +++ b/tests/main/graphql/test_main_graphql.py @@ -124,8 +124,8 @@ def test_main_use_default_kwarg(output_file: Path) -> None: ], ) @pytest.mark.skipif( - black.__version__.split(".")[0] == "19", - reason="Installed black doesn't support the old style", + black.__version__.split(".")[0] in {"19", "22"}, + reason="Installed black doesn't support Python 3.12 target version", ) def test_main_graphql_empty_list_default(output_model: str, expected_output: str, output_file: Path) -> None: """Test GraphQL generation with empty list default values."""