Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/datamodel_code_generator/model/pydantic/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,12 @@ 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 empty list defaults for any list type (including primitive types like String)
if self.data_type.is_list and isinstance(self.default, list) and not self.default:
return STANDARD_LIST
for data_type in self.data_type.data_types or (self.data_type,):
# TODO: Check nested data_types
if data_type.is_dict:
Expand Down Expand Up @@ -220,7 +223,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()
Expand Down Expand Up @@ -249,7 +252,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)
Expand Down
44 changes: 44 additions & 0 deletions tests/data/expected/main/graphql/empty_list_default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# generated by datamodel-codegen:
# filename: empty_list_default.graphql
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from typing import Literal, TypeAlias

from pydantic import BaseModel, Field

Boolean: TypeAlias = bool
"""
The `Boolean` scalar type represents `true` or `false`.
"""


ID: TypeAlias = str
"""
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.
"""


String: TypeAlias = 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 ItemInput(BaseModel):
id: ID
typename__: Literal['ItemInput'] | None = Field('ItemInput', alias='__typename')


class TestInput(BaseModel):
nullableList: list[String] | None = Field(
default_factory=list, description='Nullable list with empty default'
)
requiredItems: list[ItemInput] = Field(
default_factory=list, description='Required list of items with empty default'
)
requiredList: list[String] = Field(
default_factory=list, description='Required list with empty default'
)
typename__: Literal['TestInput'] | None = Field('TestInput', alias='__typename')
45 changes: 45 additions & 0 deletions tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# generated by datamodel-codegen:
# filename: empty_list_default.graphql
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, Field
from typing_extensions import TypeAliasType

Boolean = TypeAliasType("Boolean", bool)
"""
The `Boolean` scalar type represents `true` or `false`.
"""


ID = TypeAliasType("ID", str)
"""
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.
"""


String = TypeAliasType("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 ItemInput(BaseModel):
id: ID
typename__: Literal['ItemInput'] | None = Field('ItemInput', alias='__typename')


class TestInput(BaseModel):
nullableList: list[String] | None = Field(
default_factory=list, description='Nullable list with empty default'
)
requiredItems: list[ItemInput] = Field(
default_factory=list, description='Required list of items with empty default'
)
requiredList: list[String] = Field(
default_factory=list, description='Required list with empty default'
)
typename__: Literal['TestInput'] | None = Field('TestInput', alias='__typename')
18 changes: 18 additions & 0 deletions tests/data/graphql/empty_list_default.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
type Query {
test: String
}

input ItemInput {
id: ID!
}

input TestInput {
"Required list with empty default"
requiredList: [String!]! = []

"Nullable list with empty default"
nullableList: [String!] = []

"Required list of items with empty default"
requiredItems: [ItemInput!]! = []
}
29 changes: 29 additions & 0 deletions tests/main/graphql/test_main_graphql.py
Original file line number Diff line number Diff line change
Expand Up @@ -890,3 +890,32 @@ def test_main_graphql_no_typename(output_file: Path) -> None:
expected_file="no_typename.py",
extra_args=["--graphql-no-typename"],
)


@pytest.mark.parametrize(
("output_model", "expected_output"),
[
(
"pydantic.BaseModel",
"empty_list_default.py",
),
(
"pydantic_v2.BaseModel",
"pydantic_v2_empty_list_default.py",
),
],
)
def test_main_graphql_empty_list_default(output_model: str, expected_output: str, output_file: Path) -> None:
"""Test that empty list defaults in GraphQL input types generate default_factory=list.

This test verifies that fields like `requiredList: [String!]! = []` correctly
generate `Field(default_factory=list)` instead of `Field(...)` or `Field([])`.
"""
run_main_and_assert(
input_path=GRAPHQL_DATA_PATH / "empty_list_default.graphql",
output_path=output_file,
input_file_type="graphql",
assert_func=assert_file_content,
expected_file=expected_output,
extra_args=["--output-model-type", output_model],
)
Loading