Skip to content

Commit fe65bfc

Browse files
koxudaxiclaude
andcommitted
Fix empty list default for GraphQL list fields
When GraphQL input fields have empty list defaults (e.g., `requiredList: [String!]! = []`), the generated code was incorrectly producing: - `Field(...)` for required lists (ignoring the default) - `Field([])` for nullable lists (using mutable default) This fix ensures all list fields with empty defaults generate `Field(default_factory=list)`, which is the correct way to handle mutable defaults in Pydantic. Changes: - Add early check in `_get_default_as_pydantic_model()` for any list type with empty default - Update `__str__()` to not skip default handling when `required=True` but `has_default=True` - Update field argument generation to not add `...` when there's already a `default_factory` Fixes issue where GraphQL schemas with default empty arrays would generate incorrect Pydantic code. Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent fa1fc11 commit fe65bfc

File tree

5 files changed

+142
-3
lines changed

5 files changed

+142
-3
lines changed

src/datamodel_code_generator/model/pydantic/base_model.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,12 @@ def _get_strict_field_constraint_value(self, constraint: str, value: Any) -> Any
126126
return value
127127
return int(value)
128128

129-
def _get_default_as_pydantic_model(self) -> str | None:
129+
def _get_default_as_pydantic_model(self) -> str | None: # noqa: PLR0911, PLR0912
130130
if isinstance(self.default, WrappedDefault):
131131
return f"lambda :{self.default!r}"
132+
# Handle empty list defaults for any list type (including primitive types like String)
133+
if self.data_type.is_list and isinstance(self.default, list) and not self.default:
134+
return STANDARD_LIST
132135
for data_type in self.data_type.data_types or (self.data_type,):
133136
# TODO: Check nested data_types
134137
if data_type.is_dict:
@@ -220,7 +223,7 @@ def __str__(self) -> str: # noqa: PLR0912
220223
elif isinstance(discriminator, dict): # pragma: no cover
221224
data["discriminator"] = discriminator["propertyName"]
222225

223-
if self.required:
226+
if self.required and not self.has_default:
224227
default_factory = None
225228
elif self.default is not UNDEFINED and self.default is not None and "default_factory" not in data:
226229
default_factory = self._get_default_as_pydantic_model()
@@ -249,7 +252,7 @@ def __str__(self) -> str: # noqa: PLR0912
249252

250253
if self.use_annotated:
251254
field_arguments = self._process_annotated_field_arguments(field_arguments)
252-
elif self.required:
255+
elif self.required and not default_factory:
253256
field_arguments = ["...", *field_arguments]
254257
elif not default_factory:
255258
default_repr = repr_set_sorted(self.default) if isinstance(self.default, set) else repr(self.default)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# generated by datamodel-codegen:
2+
# filename: empty_list_default.graphql
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Literal, TypeAlias
8+
9+
from pydantic import BaseModel, Field
10+
11+
Boolean: TypeAlias = bool
12+
"""
13+
The `Boolean` scalar type represents `true` or `false`.
14+
"""
15+
16+
17+
ID: TypeAlias = str
18+
"""
19+
The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
20+
"""
21+
22+
23+
String: TypeAlias = str
24+
"""
25+
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.
26+
"""
27+
28+
29+
class ItemInput(BaseModel):
30+
id: ID
31+
typename__: Literal['ItemInput'] | None = Field('ItemInput', alias='__typename')
32+
33+
34+
class TestInput(BaseModel):
35+
nullableList: list[String] | None = Field(
36+
default_factory=list, description='Nullable list with empty default'
37+
)
38+
requiredItems: list[ItemInput] = Field(
39+
default_factory=list, description='Required list of items with empty default'
40+
)
41+
requiredList: list[String] = Field(
42+
default_factory=list, description='Required list with empty default'
43+
)
44+
typename__: Literal['TestInput'] | None = Field('TestInput', alias='__typename')
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# generated by datamodel-codegen:
2+
# filename: empty_list_default.graphql
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Literal
8+
9+
from pydantic import BaseModel, Field
10+
from typing_extensions import TypeAliasType
11+
12+
Boolean = TypeAliasType("Boolean", bool)
13+
"""
14+
The `Boolean` scalar type represents `true` or `false`.
15+
"""
16+
17+
18+
ID = TypeAliasType("ID", str)
19+
"""
20+
The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
21+
"""
22+
23+
24+
String = TypeAliasType("String", str)
25+
"""
26+
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.
27+
"""
28+
29+
30+
class ItemInput(BaseModel):
31+
id: ID
32+
typename__: Literal['ItemInput'] | None = Field('ItemInput', alias='__typename')
33+
34+
35+
class TestInput(BaseModel):
36+
nullableList: list[String] | None = Field(
37+
default_factory=list, description='Nullable list with empty default'
38+
)
39+
requiredItems: list[ItemInput] = Field(
40+
default_factory=list, description='Required list of items with empty default'
41+
)
42+
requiredList: list[String] = Field(
43+
default_factory=list, description='Required list with empty default'
44+
)
45+
typename__: Literal['TestInput'] | None = Field('TestInput', alias='__typename')
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
type Query {
2+
test: String
3+
}
4+
5+
input ItemInput {
6+
id: ID!
7+
}
8+
9+
input TestInput {
10+
"Required list with empty default"
11+
requiredList: [String!]! = []
12+
13+
"Nullable list with empty default"
14+
nullableList: [String!] = []
15+
16+
"Required list of items with empty default"
17+
requiredItems: [ItemInput!]! = []
18+
}

tests/main/graphql/test_main_graphql.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,3 +890,32 @@ def test_main_graphql_no_typename(output_file: Path) -> None:
890890
expected_file="no_typename.py",
891891
extra_args=["--graphql-no-typename"],
892892
)
893+
894+
895+
@pytest.mark.parametrize(
896+
("output_model", "expected_output"),
897+
[
898+
(
899+
"pydantic.BaseModel",
900+
"empty_list_default.py",
901+
),
902+
(
903+
"pydantic_v2.BaseModel",
904+
"pydantic_v2_empty_list_default.py",
905+
),
906+
],
907+
)
908+
def test_main_graphql_empty_list_default(output_model: str, expected_output: str, output_file: Path) -> None:
909+
"""Test that empty list defaults in GraphQL input types generate default_factory=list.
910+
911+
This test verifies that fields like `requiredList: [String!]! = []` correctly
912+
generate `Field(default_factory=list)` instead of `Field(...)` or `Field([])`.
913+
"""
914+
run_main_and_assert(
915+
input_path=GRAPHQL_DATA_PATH / "empty_list_default.graphql",
916+
output_path=output_file,
917+
input_file_type="graphql",
918+
assert_func=assert_file_content,
919+
expected_file=expected_output,
920+
extra_args=["--output-model-type", output_model],
921+
)

0 commit comments

Comments
 (0)