Skip to content

Commit df834dc

Browse files
butvinmclaude
andauthored
Use validate_default=True for structured union defaults (#3050)
* Use validate_default=True instead of model_validate/TypeAdapter for structured defaults Replace the ValidatedDefault/TypeAdapter mechanism from #3040 and the original model_validate() codegen with Pydantic's native validate_default=True. Instead of generating complex default_factory lambdas like: default_factory=lambda: A.model_validate({'type': 'b'}) default_factory=lambda: TypeAdapter(A | B).validate_python({'type': 'b'}) Now generates: Field({'type': 'b'}, validate_default=True) This lets Pydantic handle union branch selection, dict-to-model coercion, and RootModel wrapping at runtime. Fixes #3034, fixes #3035. Removed: - ValidatedDefault class and SUPPORTS_VALIDATED_DEFAULT flag - TypeAdapter import in generated code - 70-line branch-matching loop in _get_default_as_pydantic_model - Helper functions: _supports_validated_default_model, _uses_existing_model_factory_path, _default_matches_plain_container_branch, _get_validated_default_type_name Added: - _needs_validate_default: simple check (is default dict/list/scalar and does the type contain a model reference?) - __set_validate_default_on_fields: sets extras["validate_default"] = True Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove dead None guard from _needs_validate_default to fix coverage The caller already filters None/UNDEFINED defaults before calling this function, so the None check was unreachable. Removed the unused `default` parameter entirely. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove WrappedDefault in favor of validate_default=True for RootModel defaults WrappedDefault wrapped RootModel scalar defaults (e.g., CountType(10)) in default_factory lambdas. Since validate_default=True handles this natively, the entire mechanism is redundant. Removed: WrappedDefault class, _types.py module, SUPPORTS_WRAPPED_DEFAULT flag, __wrap_root_model_default_values method, _get_default_as_pydantic_model method. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Make GraphQL scalar types extend TypeAliasBase _DataTypeScalarBase generated the same type alias syntax as TypeAliasBase but didn't inherit from it, causing _unwrap_type_alias to not see through scalar references. This led to unnecessary validate_default=True on fields like `String | None = 'default_user'`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix required fields with validate_default losing their default value When a required field had validate_default=True set (e.g., empty list default on list[Container]), the rendering logic prepended ... instead of the actual default value. Now skips the ... override when validate_default is set, since the default is needed for Pydantic to validate at runtime. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix inaccurate docstring in test_needs_validate_default test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 76439c3 commit df834dc

File tree

54 files changed

+144
-471
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+144
-471
lines changed

src/datamodel_code_generator/model/_types.py

Lines changed: 0 additions & 30 deletions
This file was deleted.

src/datamodel_code_generator/model/base.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
IMPORT_UNION,
2626
Import,
2727
)
28-
from datamodel_code_generator.model._types import ValidatedDefault, WrappedDefault
2928
from datamodel_code_generator.reference import Reference, _BaseModel
3029
from datamodel_code_generator.types import (
3130
ANY,
@@ -38,8 +37,6 @@
3837
get_optional_type,
3938
)
4039

41-
__all__ = ["ValidatedDefault", "WrappedDefault"]
42-
4340
if TYPE_CHECKING:
4441
from collections.abc import Iterator
4542

@@ -598,8 +595,6 @@ class DataModel(TemplateBase, Nullable, ABC): # noqa: PLR0904
598595
SUPPORTS_GENERIC_BASE_CLASS: ClassVar[bool] = True
599596
SUPPORTS_DISCRIMINATOR: ClassVar[bool] = False
600597
SUPPORTS_FIELD_RENAMING: ClassVar[bool] = False
601-
SUPPORTS_WRAPPED_DEFAULT: ClassVar[bool] = False
602-
SUPPORTS_VALIDATED_DEFAULT: ClassVar[bool] = False
603598
SUPPORTS_KW_ONLY: ClassVar[bool] = False
604599
REQUIRES_RUNTIME_IMPORTS_WITH_RUFF_CHECK: ClassVar[bool] = False
605600
has_forward_reference: bool = False

src/datamodel_code_generator/model/pydantic_base.py

Lines changed: 7 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,13 @@
1919
DataModel,
2020
DataModelFieldBase,
2121
)
22-
from datamodel_code_generator.model._types import ValidatedDefault, WrappedDefault
2322
from datamodel_code_generator.model.base import UNDEFINED, repr_set_sorted
24-
from datamodel_code_generator.types import STANDARD_DICT, STANDARD_LIST, UnionIntFloat, chain_as_tuple
23+
from datamodel_code_generator.types import UnionIntFloat, chain_as_tuple
2524

2625
# Defined here instead of importing from pydantic_v2.imports to avoid circular import
2726
# (pydantic_base -> pydantic_v2.imports -> pydantic_v2/__init__ -> pydantic_v2.base_model -> pydantic_base)
2827
IMPORT_ANYURL = Import.from_full_path("pydantic.AnyUrl")
2928
IMPORT_FIELD = Import.from_full_path("pydantic.Field")
30-
IMPORT_TYPE_ADAPTER = Import.from_full_path("pydantic.TypeAdapter")
3129

3230
if TYPE_CHECKING:
3331
from collections import defaultdict
@@ -126,81 +124,6 @@ def _get_strict_field_constraint_value(self, constraint: str, value: Any) -> Any
126124
return value
127125
return int(value)
128126

129-
def _get_default_as_pydantic_model(self) -> str | None: # noqa: PLR0911, PLR0912
130-
if isinstance(self.default, (ValidatedDefault, WrappedDefault)):
131-
return f"lambda :{self.default!r}"
132-
if self.data_type.is_list and len(self.data_type.data_types) == 1:
133-
data_type_child = self.data_type.data_types[0]
134-
if (
135-
data_type_child.reference
136-
and isinstance(data_type_child.reference.source, BaseModelBase)
137-
and isinstance(self.default, list)
138-
):
139-
if not self.default:
140-
return STANDARD_LIST
141-
return ( # pragma: no cover
142-
f"lambda :[{data_type_child.alias or data_type_child.reference.source.class_name}."
143-
f"{self._PARSE_METHOD}(v) for v in {self.default!r}]"
144-
)
145-
if self.data_type.is_dict and len(self.data_type.data_types) == 1:
146-
data_type_value = self.data_type.data_types[0]
147-
if (
148-
data_type_value.reference
149-
and isinstance(data_type_value.reference.source, BaseModelBase)
150-
and isinstance(self.default, dict)
151-
):
152-
if not self.default:
153-
return STANDARD_DICT
154-
return (
155-
f"lambda :{{k: {data_type_value.alias or data_type_value.reference.source.class_name}."
156-
f"{self._PARSE_METHOD}(v) for k, v in {self.default!r}.items()}}"
157-
)
158-
159-
for data_type in self.data_type.data_types or (self.data_type,):
160-
# TODO: Check nested data_types
161-
if data_type.is_dict:
162-
if len(data_type.data_types) == 1:
163-
data_type_value = data_type.data_types[0]
164-
if (
165-
data_type_value.reference
166-
and isinstance(data_type_value.reference.source, BaseModelBase)
167-
and isinstance(self.default, dict)
168-
):
169-
if not self.default:
170-
return STANDARD_DICT
171-
return (
172-
f"lambda :{{k: {data_type_value.alias or data_type_value.reference.source.class_name}."
173-
f"{self._PARSE_METHOD}(v) for k, v in {self.default!r}.items()}}"
174-
)
175-
continue
176-
if data_type.is_list and len(data_type.data_types) == 1:
177-
data_type_child = data_type.data_types[0]
178-
if (
179-
data_type_child.reference
180-
and isinstance(data_type_child.reference.source, BaseModelBase)
181-
and isinstance(self.default, list)
182-
): # pragma: no cover
183-
if not self.default:
184-
return STANDARD_LIST
185-
return (
186-
f"lambda :[{data_type_child.alias or data_type_child.reference.source.class_name}."
187-
f"{self._PARSE_METHOD}(v) for v in {self.default!r}]"
188-
)
189-
elif data_type.reference and isinstance(data_type.reference.source, BaseModelBase):
190-
source = data_type.reference.source
191-
is_root_model = hasattr(source, "BASE_CLASS") and source.BASE_CLASS == "pydantic.RootModel"
192-
if self.data_type.is_union:
193-
if not isinstance(self.default, (dict, list)):
194-
if not is_root_model:
195-
continue
196-
elif isinstance(self.default, dict) and any(dt.is_dict for dt in self.data_type.data_types):
197-
continue
198-
class_name = data_type.alias or source.class_name
199-
if is_root_model:
200-
return f"lambda :{class_name}({self.default!r})"
201-
return f"lambda :{class_name}.{self._PARSE_METHOD}({self.default!r})"
202-
return None
203-
204127
def _get_default_factory_for_optional_nested_model(self) -> str | None:
205128
"""Get default_factory for optional nested Pydantic model fields.
206129
@@ -259,10 +182,10 @@ def __str__(self) -> str: # noqa: PLR0912
259182
elif isinstance(discriminator, dict): # pragma: no cover
260183
data["discriminator"] = discriminator["propertyName"]
261184

262-
if self.required and not self.has_default:
185+
if (self.required and not self.has_default) or (
186+
self.default is not UNDEFINED and self.default is not None and "default_factory" not in data
187+
):
263188
default_factory = None
264-
elif self.default is not UNDEFINED and self.default is not None and "default_factory" not in data:
265-
default_factory = self._get_default_as_pydantic_model()
266189
else:
267190
default_factory = data.pop("default_factory", None)
268191

@@ -288,7 +211,7 @@ def __str__(self) -> str: # noqa: PLR0912
288211

289212
if self.use_annotated:
290213
field_arguments = self._process_annotated_field_arguments(field_arguments)
291-
elif self.required and not default_factory:
214+
elif self.required and not default_factory and not self.extras.get("validate_default"):
292215
field_arguments = ["...", *field_arguments]
293216
elif not default_factory:
294217
default_repr = repr_set_sorted(self.default) if isinstance(self.default, set) else repr(self.default)
@@ -323,12 +246,8 @@ def annotated(self) -> str | None:
323246
@property
324247
def imports(self) -> tuple[Import, ...]:
325248
"""Get all required imports including Field if needed."""
326-
field = self.field
327-
if field:
328-
extra_imports = [IMPORT_FIELD]
329-
if isinstance(self.default, ValidatedDefault):
330-
extra_imports.append(IMPORT_TYPE_ADAPTER)
331-
return chain_as_tuple(super().imports, extra_imports)
249+
if self.field:
250+
return chain_as_tuple(super().imports, (IMPORT_FIELD,))
332251
return super().imports
333252

334253

src/datamodel_code_generator/model/pydantic_v2/base_model.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,6 @@ class BaseModel(BaseModelBase):
222222
BASE_CLASS_ALIAS: ClassVar[str] = "_BaseModel"
223223
SUPPORTS_DISCRIMINATOR: ClassVar[bool] = True
224224
SUPPORTS_FIELD_RENAMING: ClassVar[bool] = True
225-
SUPPORTS_WRAPPED_DEFAULT: ClassVar[bool] = True
226-
SUPPORTS_VALIDATED_DEFAULT: ClassVar[bool] = True
227225
# In Pydantic 2.11+, populate_by_name is deprecated in favor of validate_by_name + validate_by_alias
228226
# Default to V2 compatible (populate_by_name) unless target_pydantic_version is specified
229227
_CONFIG_ATTRIBUTES_V2: ClassVar[list[ConfigAttribute]] = [

src/datamodel_code_generator/model/scalar.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
IMPORT_TYPE_ALIAS_TYPE,
1414
Import,
1515
)
16-
from datamodel_code_generator.model import DataModel, DataModelFieldBase
1716
from datamodel_code_generator.model.base import UNDEFINED
17+
from datamodel_code_generator.model.type_alias import TypeAliasBase
1818

1919
if TYPE_CHECKING:
2020
from pathlib import Path
2121

22+
from datamodel_code_generator.model import DataModelFieldBase
2223
from datamodel_code_generator.reference import Reference
2324

2425
_INT: str = "int"
@@ -38,7 +39,7 @@
3839
}
3940

4041

41-
class _DataTypeScalarBase(DataModel):
42+
class _DataTypeScalarBase(TypeAliasBase):
4243
"""Base class for GraphQL scalar types with shared __init__ logic."""
4344

4445
def __init__( # noqa: PLR0913

0 commit comments

Comments
 (0)