From 289dc17081d8da503ba3bfe85a3b0dbd9aadbeb4 Mon Sep 17 00:00:00 2001 From: Mikhail Butvin Date: Fri, 13 Mar 2026 09:35:10 +0300 Subject: [PATCH 1/6] 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 --- src/datamodel_code_generator/model/_types.py | 12 --- src/datamodel_code_generator/model/base.py | 5 +- .../model/pydantic_base.py | 87 ++-------------- .../model/pydantic_v2/base_model.py | 1 - src/datamodel_code_generator/parser/base.py | 98 ++++--------------- .../default_values_required_use_default.py | 4 +- .../graphql/pydantic_v2_empty_list_default.py | 6 +- .../all_of_any_of_base_class_ref.py | 2 +- .../has_default_value_pydantic_v2.py | 10 +- .../jsonschema_root_model_ordering.py | 8 +- ...ma_root_model_ordering_keep_model_order.py | 8 +- .../pydantic_v2_model_default_dict_empty.py | 2 +- ...ydantic_v2_model_default_dict_non_empty.py | 8 +- ...ic_v2_model_default_nullable_dict_empty.py | 2 +- ...2_model_default_nullable_dict_non_empty.py | 8 +- .../root_model_default_value_branches.py | 5 +- .../root_model_default_value_no_annotated.py | 4 +- .../root_model_default_value_non_root.py | 7 +- tests/data/expected/main/jsonschema/titles.py | 5 +- .../jsonschema/titles_use_title_as_name.py | 8 +- ...pe_alias_chain_model_default_object_ref.py | 6 +- ...pe_alias_chain_union_default_object_ref.py | 9 +- ...ype_alias_dict_union_default_object_ref.py | 13 +-- ..._alias_inline_dict_union_default_object.py | 13 +-- ..._alias_inline_list_union_default_object.py | 11 +-- .../type_alias_inline_union_default_object.py | 6 +- ...e_union_default_object_import_collision.py | 9 +- ...lias_inline_union_default_object_one_of.py | 9 +- ...nion_default_object_silent_wrong_branch.py | 14 +-- ...line_union_default_object_type_override.py | 11 +-- ...ype_alias_list_model_default_object_ref.py | 6 +- ...ype_alias_list_union_default_object_ref.py | 11 +-- .../type_alias_model_default_object_ref.py | 6 +- ..._nullable_list_model_default_object_ref.py | 6 +- ...alias_nullable_model_default_object_ref.py | 6 +- .../type_alias_union_default_object_ref.py | 6 +- ...e_alias_union_default_object_ref_any_of.py | 7 +- ...on_default_object_ref_dict_alias_branch.py | 6 +- ...s_union_default_object_ref_mixed_scalar.py | 14 +-- ...e_alias_union_default_object_ref_one_of.py | 7 +- .../without_titles_use_title_as_name.py | 2 +- .../default_values_parameters_use_default.py | 2 +- .../expected/main/openapi/enum_models/one.py | 4 +- .../enum_models/one_literal_as_default.py | 4 +- .../pydantic_v2_default_object/Another.py | 6 +- .../pydantic_v2_default_object/Nested.py | 12 +-- .../pydantic_v2_default_object/__init__.py | 10 +- .../openapi/pydantic_v2_empty_list_default.py | 2 +- .../main/openapi/referenced_default.py | 2 +- .../openapi/root_model_default_primitive.py | 2 +- .../output.py | 4 +- tests/parser/test_base.py | 22 ++--- 52 files changed, 139 insertions(+), 399 deletions(-) diff --git a/src/datamodel_code_generator/model/_types.py b/src/datamodel_code_generator/model/_types.py index 0ca60c613..e2b96fecf 100644 --- a/src/datamodel_code_generator/model/_types.py +++ b/src/datamodel_code_generator/model/_types.py @@ -16,15 +16,3 @@ 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 ef822c9bd..711e1aeb1 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 ValidatedDefault, WrappedDefault +from datamodel_code_generator.model._types import WrappedDefault from datamodel_code_generator.reference import Reference, _BaseModel from datamodel_code_generator.types import ( ANY, @@ -38,7 +38,7 @@ get_optional_type, ) -__all__ = ["ValidatedDefault", "WrappedDefault"] +__all__ = ["WrappedDefault"] if TYPE_CHECKING: from collections.abc import Iterator @@ -599,7 +599,6 @@ 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 REQUIRES_RUNTIME_IMPORTS_WITH_RUFF_CHECK: 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 d245aaee4..189f0146b 100644 --- a/src/datamodel_code_generator/model/pydantic_base.py +++ b/src/datamodel_code_generator/model/pydantic_base.py @@ -19,15 +19,14 @@ DataModel, DataModelFieldBase, ) -from datamodel_code_generator.model._types import ValidatedDefault, WrappedDefault +from datamodel_code_generator.model._types import WrappedDefault from datamodel_code_generator.model.base import UNDEFINED, repr_set_sorted -from datamodel_code_generator.types import STANDARD_DICT, STANDARD_LIST, UnionIntFloat, chain_as_tuple +from datamodel_code_generator.types import UnionIntFloat, chain_as_tuple # Defined here instead of importing from pydantic_v2.imports to avoid circular import # (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,79 +125,9 @@ 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: # noqa: PLR0911, PLR0912 - if isinstance(self.default, (ValidatedDefault, WrappedDefault)): + def _get_default_as_pydantic_model(self) -> str | None: + if isinstance(self.default, 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] - 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}]" - ) - if self.data_type.is_dict and len(self.data_type.data_types) == 1: - data_type_value = self.data_type.data_types[0] - if ( - data_type_value.reference - and isinstance(data_type_value.reference.source, BaseModelBase) - and isinstance(self.default, dict) - ): - if not self.default: - return STANDARD_DICT - return ( - f"lambda :{{k: {data_type_value.alias or data_type_value.reference.source.class_name}." - f"{self._PARSE_METHOD}(v) for k, v in {self.default!r}.items()}}" - ) - - for data_type in self.data_type.data_types or (self.data_type,): - # TODO: Check nested data_types - if data_type.is_dict: - if len(data_type.data_types) == 1: - data_type_value = data_type.data_types[0] - if ( - data_type_value.reference - and isinstance(data_type_value.reference.source, BaseModelBase) - and isinstance(self.default, dict) - ): - if not self.default: - return STANDARD_DICT - return ( - f"lambda :{{k: {data_type_value.alias or data_type_value.reference.source.class_name}." - f"{self._PARSE_METHOD}(v) for k, v in {self.default!r}.items()}}" - ) - continue - if data_type.is_list and len(data_type.data_types) == 1: - data_type_child = data_type.data_types[0] - if ( - data_type_child.reference - and isinstance(data_type_child.reference.source, BaseModelBase) - and isinstance(self.default, list) - ): # pragma: no cover - if not self.default: - return STANDARD_LIST - return ( - 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}]" - ) - elif data_type.reference and isinstance(data_type.reference.source, BaseModelBase): - source = data_type.reference.source - is_root_model = hasattr(source, "BASE_CLASS") and source.BASE_CLASS == "pydantic.RootModel" - if self.data_type.is_union: - if not isinstance(self.default, (dict, list)): - if not is_root_model: - continue - elif isinstance(self.default, dict) and any(dt.is_dict for dt in self.data_type.data_types): - continue - class_name = data_type.alias or source.class_name - if is_root_model: - return f"lambda :{class_name}({self.default!r})" - return f"lambda :{class_name}.{self._PARSE_METHOD}({self.default!r})" return None def _get_default_factory_for_optional_nested_model(self) -> str | None: @@ -323,12 +252,8 @@ def annotated(self) -> str | None: @property def imports(self) -> tuple[Import, ...]: """Get all required imports including Field if needed.""" - 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) + if self.field: + return chain_as_tuple(super().imports, (IMPORT_FIELD,)) 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 45ae53a2d..3fb3078d3 100644 --- a/src/datamodel_code_generator/model/pydantic_v2/base_model.py +++ b/src/datamodel_code_generator/model/pydantic_v2/base_model.py @@ -223,7 +223,6 @@ 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 95f7c1a43..581468e2f 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -75,7 +75,6 @@ ConstraintsBase, DataModel, DataModelFieldBase, - ValidatedDefault, WrappedDefault, ) from datamodel_code_generator.model.enum import Enum, Member @@ -84,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, NONE, DataType, DataTypeManager, _remove_none_from_union +from datamodel_code_generator.types import ANY, DataType, DataTypeManager from datamodel_code_generator.util import camel_to_snake if TYPE_CHECKING: @@ -402,6 +401,7 @@ def iter_models_field_data_types( def _unwrap_type_alias(data_type: DataType) -> DataType: + """Follow type alias references to the underlying data type.""" current = data_type seen: set[int] = set() while current.reference and isinstance(current.reference.source, TypeAliasBase): @@ -413,83 +413,35 @@ 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: + """Check if a data type tree contains any reference to a non-alias model.""" 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 _supports_validated_default_model(resolved.reference.source): + if ( + resolved.reference + and isinstance(resolved.reference.source, DataModel) + and not isinstance(resolved.reference.source, Enum) + and not resolved.reference.source.is_alias + ): 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 _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 _supports_validated_default_model(candidate.data_types[0].reference.source) - ): - return True - if ( - candidate.is_dict - and len(candidate.data_types) == 1 - and candidate.data_types[0].reference - and _supports_validated_default_model(candidate.data_types[0].reference.source) - ): - return True - return False - - -def _default_matches_plain_container_branch(default: Any, data_type: DataType) -> bool: - 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) - ) - - -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)): +def _needs_validate_default(default: Any, data_type: DataType) -> bool: + """Check if a field needs validate_default=True to coerce defaults into model instances.""" + if default is None: 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 resolved.data_types if branch.type_hint != NONE) - # 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) - - return not _uses_existing_model_factory_path(data_type) + return _contains_model_reference(resolved) def _alias_base_class_imports( @@ -2246,30 +2198,22 @@ def __wrap_root_model_default_values( type_name=type_name, ) - def __wrap_validated_default_values( + def __set_validate_default_on_fields( # noqa: PLR6301 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 + """Set validate_default=True on fields with structured defaults needing validation.""" for model in models: - if isinstance(model, (Enum, self.data_model_root_type)): + if isinstance(model, Enum): continue for model_field in model.fields: - if model_field.default is None: + if model_field.default is None or model_field.default is UNDEFINED: continue 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): + if not _needs_validate_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), - ) + model_field.extras["validate_default"] = True def __override_required_field( self, @@ -3315,7 +3259,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) + self.__set_validate_default_on_fields(models) return ModuleContext(module, module_, models, is_init, imports, scoped_model_resolver) @@ -3348,7 +3292,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) + self.__set_validate_default_on_fields(ctx.models) def _generate_module_output( # noqa: PLR0913, PLR0917 self, diff --git a/tests/data/expected/main/graphql/default_values_required_use_default.py b/tests/data/expected/main/graphql/default_values_required_use_default.py index e9fed1bf3..9992067ae 100644 --- a/tests/data/expected/main/graphql/default_values_required_use_default.py +++ b/tests/data/expected/main/graphql/default_values_required_use_default.py @@ -29,6 +29,6 @@ class User(BaseModel): id: ID - name: String | None = 'default_user' - status: String | None = 'active' + name: String | None = Field('default_user', validate_default=True) + status: String | None = Field('active', validate_default=True) typename__: Literal['User'] | None = Field('User', 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 8def8a752..1846955e3 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 @@ -26,9 +26,9 @@ class Container(BaseModel): class PodSpec(BaseModel): - container_list: list[Container] = Field(default_factory=list) - container_list_or_none: list[Container | None] = Field(default_factory=list) + container_list: list[Container] = Field(..., validate_default=True) + container_list_or_none: list[Container | None] = Field(..., validate_default=True) container_or_none_list_or_none: list[Container | None] | None = Field( - default_factory=list + [], validate_default=True ) typename__: Literal['PodSpec'] | None = Field('PodSpec', alias='__typename') diff --git a/tests/data/expected/main/jsonschema/all_of_any_of_base_class_ref.py b/tests/data/expected/main/jsonschema/all_of_any_of_base_class_ref.py index a7fa7a9fd..da4253aa2 100644 --- a/tests/data/expected/main/jsonschema/all_of_any_of_base_class_ref.py +++ b/tests/data/expected/main/jsonschema/all_of_any_of_base_class_ref.py @@ -16,7 +16,7 @@ class MapState1(BaseModel): class MapState2(BaseModel): latitude: Latitude longitude: Longitude - zoom: Zoom | None = Field(default_factory=lambda: Zoom(0)) + zoom: Zoom | None = Field(0, validate_default=True) bearing: Bearing | None = None pitch: Pitch drag_rotate: DragRotate | None = Field(None, alias="dragRotate") diff --git a/tests/data/expected/main/jsonschema/has_default_value_pydantic_v2.py b/tests/data/expected/main/jsonschema/has_default_value_pydantic_v2.py index 317265c98..d8bacb140 100644 --- a/tests/data/expected/main/jsonschema/has_default_value_pydantic_v2.py +++ b/tests/data/expected/main/jsonschema/has_default_value_pydantic_v2.py @@ -25,21 +25,17 @@ class Pet(BaseModel): class Family(RootModel[list[ID]]): - root: list[ID] = Field( - default_factory=lambda: [ID.model_validate(v) for v in ['abc', 'efg']] - ) + root: list[ID] = Field(['abc', 'efg'], validate_default=True) class FamilyPets(RootModel[list[Pet]]): root: list[Pet] = Field( - default_factory=lambda: [ - Pet.model_validate(v) for v in [{'name': 'taro'}, {'name': 'shiro'}] - ] + [{'name': 'taro'}, {'name': 'shiro'}], validate_default=True ) class Person(BaseModel): - id: ID | None = Field(default_factory=lambda: ID('abc')) + id: ID | None = Field('abc', validate_default=True) user: Pet | None = None firstName: str | None = Field(None, description="The person's first name.") team: TeamType | None = 'Department' diff --git a/tests/data/expected/main/jsonschema/jsonschema_root_model_ordering.py b/tests/data/expected/main/jsonschema/jsonschema_root_model_ordering.py index 9d84e2e11..7957ccc95 100644 --- a/tests/data/expected/main/jsonschema/jsonschema_root_model_ordering.py +++ b/tests/data/expected/main/jsonschema/jsonschema_root_model_ordering.py @@ -10,22 +10,22 @@ class Zoo(BaseModel): - animals: list[Animals] | None = Field(default_factory=list, title='Animals') + animals: list[Animals] | None = Field([], title='Animals', validate_default=True) class Dog(BaseModel): name: Literal['dog'] = Field('dog', title='woof') - friends: list[Friends] | None = Field(default_factory=list, title='Friends') + friends: list[Friends] | None = Field([], title='Friends', validate_default=True) class Cat(BaseModel): name: Literal['cat'] = Field('cat', title='meow') - friends: list[Friends] | None = Field(default_factory=list, title='Friends') + friends: list[Friends] | None = Field([], title='Friends', validate_default=True) class Bird(BaseModel): name: Literal['bird'] = Field('bird', title='chirp') - friends: list[Friends] | None = Field(default_factory=list, title='Friends') + friends: list[Friends] | None = Field([], title='Friends', validate_default=True) class Animals(RootModel[Dog | Cat | Bird]): diff --git a/tests/data/expected/main/jsonschema/jsonschema_root_model_ordering_keep_model_order.py b/tests/data/expected/main/jsonschema/jsonschema_root_model_ordering_keep_model_order.py index 9205f0e14..618fe3a35 100644 --- a/tests/data/expected/main/jsonschema/jsonschema_root_model_ordering_keep_model_order.py +++ b/tests/data/expected/main/jsonschema/jsonschema_root_model_ordering_keep_model_order.py @@ -11,17 +11,17 @@ class Bird(BaseModel): name: Literal['bird'] = Field('bird', title='chirp') - friends: list[Friends] | None = Field(default_factory=list, title='Friends') + friends: list[Friends] | None = Field([], title='Friends', validate_default=True) class Cat(BaseModel): name: Literal['cat'] = Field('cat', title='meow') - friends: list[Friends] | None = Field(default_factory=list, title='Friends') + friends: list[Friends] | None = Field([], title='Friends', validate_default=True) class Dog(BaseModel): name: Literal['dog'] = Field('dog', title='woof') - friends: list[Friends] | None = Field(default_factory=list, title='Friends') + friends: list[Friends] | None = Field([], title='Friends', validate_default=True) class Animals(RootModel[Dog | Cat | Bird]): @@ -33,7 +33,7 @@ class Friends(RootModel[Dog | Cat | Bird]): class Zoo(BaseModel): - animals: list[Animals] | None = Field(default_factory=list, title='Animals') + animals: list[Animals] | None = Field([], title='Animals', validate_default=True) Bird.model_rebuild() diff --git a/tests/data/expected/main/jsonschema/pydantic_v2_model_default_dict_empty.py b/tests/data/expected/main/jsonschema/pydantic_v2_model_default_dict_empty.py index 1914b8394..00a876a09 100644 --- a/tests/data/expected/main/jsonschema/pydantic_v2_model_default_dict_empty.py +++ b/tests/data/expected/main/jsonschema/pydantic_v2_model_default_dict_empty.py @@ -14,5 +14,5 @@ class ItemModel(BaseModel): class ParentModel(BaseModel): dict_with_defaults: dict[str, ItemModel] | None = Field( - default_factory=dict, title='Dict With Defaults' + {}, title='Dict With Defaults', validate_default=True ) diff --git a/tests/data/expected/main/jsonschema/pydantic_v2_model_default_dict_non_empty.py b/tests/data/expected/main/jsonschema/pydantic_v2_model_default_dict_non_empty.py index 280273320..7bd0078ff 100644 --- a/tests/data/expected/main/jsonschema/pydantic_v2_model_default_dict_non_empty.py +++ b/tests/data/expected/main/jsonschema/pydantic_v2_model_default_dict_non_empty.py @@ -14,11 +14,7 @@ class ItemModel(BaseModel): class ParentModel(BaseModel): dict_with_defaults: dict[str, ItemModel] | None = Field( - default_factory=lambda: { - k: ItemModel.model_validate(v) - for k, v in { - 'jedi': {'description': 'The wise old jedi', 'name': 'Yoda'} - }.items() - }, + {'jedi': {'description': 'The wise old jedi', 'name': 'Yoda'}}, title='Dict With Defaults', + validate_default=True, ) diff --git a/tests/data/expected/main/jsonschema/pydantic_v2_model_default_nullable_dict_empty.py b/tests/data/expected/main/jsonschema/pydantic_v2_model_default_nullable_dict_empty.py index c0403321f..3325ab825 100644 --- a/tests/data/expected/main/jsonschema/pydantic_v2_model_default_nullable_dict_empty.py +++ b/tests/data/expected/main/jsonschema/pydantic_v2_model_default_nullable_dict_empty.py @@ -14,5 +14,5 @@ class ItemModel(BaseModel): class ParentModel(BaseModel): dict_with_defaults: dict[str, ItemModel] | None = Field( - default_factory=dict, title='Dict With Defaults' + {}, title='Dict With Defaults', validate_default=True ) diff --git a/tests/data/expected/main/jsonschema/pydantic_v2_model_default_nullable_dict_non_empty.py b/tests/data/expected/main/jsonschema/pydantic_v2_model_default_nullable_dict_non_empty.py index 397a6d690..2e6244d62 100644 --- a/tests/data/expected/main/jsonschema/pydantic_v2_model_default_nullable_dict_non_empty.py +++ b/tests/data/expected/main/jsonschema/pydantic_v2_model_default_nullable_dict_non_empty.py @@ -14,11 +14,7 @@ class ItemModel(BaseModel): class ParentModel(BaseModel): dict_with_defaults: dict[str, ItemModel] | None = Field( - default_factory=lambda: { - k: ItemModel.model_validate(v) - for k, v in { - 'jedi': {'description': 'The wise old jedi', 'name': 'Yoda'} - }.items() - }, + {'jedi': {'description': 'The wise old jedi', 'name': 'Yoda'}}, title='Dict With Defaults', + validate_default=True, ) diff --git a/tests/data/expected/main/jsonschema/root_model_default_value_branches.py b/tests/data/expected/main/jsonschema/root_model_default_value_branches.py index d32dc962d..c22d7a048 100644 --- a/tests/data/expected/main/jsonschema/root_model_default_value_branches.py +++ b/tests/data/expected/main/jsonschema/root_model_default_value_branches.py @@ -19,6 +19,5 @@ class Model(BaseModel): ] count_no_default: CountType | None = None count_list_default: Annotated[ - list[CountType] | None, - Field(default_factory=lambda: [CountType.model_validate(v) for v in [1, 2, 3]]), - ] + list[CountType] | None, Field(validate_default=True) + ] = [1, 2, 3] diff --git a/tests/data/expected/main/jsonschema/root_model_default_value_no_annotated.py b/tests/data/expected/main/jsonschema/root_model_default_value_no_annotated.py index f43f1e381..97dc9e11d 100644 --- a/tests/data/expected/main/jsonschema/root_model_default_value_no_annotated.py +++ b/tests/data/expected/main/jsonschema/root_model_default_value_no_annotated.py @@ -24,5 +24,5 @@ class NameType(RootModel[constr(min_length=1, max_length=50)]): class Model(BaseModel): admin_state: AdminStateLeaf | None = AdminStateLeaf.enable - count: CountType | None = Field(default_factory=lambda: CountType(10)) - name: NameType | None = Field(default_factory=lambda: NameType('default_name')) + count: CountType | None = Field(10, validate_default=True) + name: NameType | None = Field('default_name', validate_default=True) diff --git a/tests/data/expected/main/jsonschema/root_model_default_value_non_root.py b/tests/data/expected/main/jsonschema/root_model_default_value_non_root.py index c142c6d64..f3753d86b 100644 --- a/tests/data/expected/main/jsonschema/root_model_default_value_non_root.py +++ b/tests/data/expected/main/jsonschema/root_model_default_value_non_root.py @@ -21,8 +21,7 @@ class Model(BaseModel): root_model_field: Annotated[ CountType | None, Field(default_factory=lambda: CountType(10)) ] - non_root_model_field: Annotated[ - PersonType | None, - Field(default_factory=lambda: PersonType.model_validate({'name': 'John'})), - ] + non_root_model_field: Annotated[PersonType | None, Field(validate_default=True)] = { + 'name': 'John' + } primitive_field: str | None = 'hello' diff --git a/tests/data/expected/main/jsonschema/titles.py b/tests/data/expected/main/jsonschema/titles.py index 53cae4de2..8f6ace405 100644 --- a/tests/data/expected/main/jsonschema/titles.py +++ b/tests/data/expected/main/jsonschema/titles.py @@ -31,10 +31,7 @@ class ProcessingStatusUnion(BaseModel): class ProcessingTask(BaseModel): processing_status_union: ( ProcessingStatusUnion | ExtendedProcessingTask | ProcessingStatus | None - ) = Field( - default_factory=lambda: ExtendedProcessingTask('COMPLETED'), - title='Processing Status Union Title', - ) + ) = Field('COMPLETED', title='Processing Status Union Title', validate_default=True) processing_status: ProcessingStatus | None = 'COMPLETED' name: str | None = None kind: Kind | None = None diff --git a/tests/data/expected/main/jsonschema/titles_use_title_as_name.py b/tests/data/expected/main/jsonschema/titles_use_title_as_name.py index 98fbb147e..af10c2f52 100644 --- a/tests/data/expected/main/jsonschema/titles_use_title_as_name.py +++ b/tests/data/expected/main/jsonschema/titles_use_title_as_name.py @@ -30,8 +30,7 @@ class ProcessingStatusDetail(BaseModel): class ProcessingTaskTitle(BaseModel): processing_status_union: ProcessingStatusUnionTitle | None = Field( - default_factory=lambda: ProcessingStatusUnionTitle('COMPLETED'), - title='Processing Status Union Title', + 'COMPLETED', title='Processing Status Union Title', validate_default=True ) processing_status: ProcessingStatusTitle | None = 'COMPLETED' name: str | None = None @@ -58,10 +57,7 @@ class ProcessingStatusUnionTitle( RootModel[ProcessingStatusDetail | ExtendedProcessingTask | ProcessingStatusTitle] ): root: ProcessingStatusDetail | ExtendedProcessingTask | ProcessingStatusTitle = ( - Field( - default_factory=lambda: ExtendedProcessingTask('COMPLETED'), - title='Processing Status Union Title', - ) + Field('COMPLETED', title='Processing Status Union Title', validate_default=True) ) 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 index 513a02386..66dc8ae79 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -20,6 +20,4 @@ class A(BaseModel): class Model(BaseModel): - x: Alias2 | None = Field( - default_factory=lambda: TypeAdapter(Alias2).validate_python({'type': 'a'}) - ) + x: Alias2 | None = Field({'type': 'a'}, validate_default=True) 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 index 6454ab364..5a610ab15 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Annotated, Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -24,9 +24,4 @@ class B(BaseModel): class Model(BaseModel): - x_value: Annotated[ - Alias2 | None, - Field( - default_factory=lambda: TypeAdapter(Alias2).validate_python({'type': 'b'}) - ), - ] + x_value: Annotated[Alias2 | None, Field(validate_default=True)] = {'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 index d0c1b2935..8b8d50cfa 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Annotated, Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -22,11 +22,6 @@ class B(BaseModel): class Model(BaseModel): - x_value: Annotated[ - Alias | None, - Field( - default_factory=lambda: TypeAdapter(Alias).validate_python( - {'k': {'type': 'b', 'value': 1}} - ) - ), - ] + x_value: Annotated[Alias | None, Field(validate_default=True)] = { + '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 index 6a440b719..6e246a4fe 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Annotated, Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -19,11 +19,6 @@ class B(BaseModel): 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}} - ) - ), - ] + x_value: Annotated[dict[str, A | B] | None, Field(validate_default=True)] = { + '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 index 56bd5e7e8..402a9b008 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Annotated, Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -19,11 +19,6 @@ class B(BaseModel): class Model(BaseModel): - x_value: Annotated[ - list[A | B] | None, - Field( - default_factory=lambda: TypeAdapter(list[A | B]).validate_python( - [{'type': 'b', 'value': 1}] - ) - ), + x_value: Annotated[list[A | B] | None, Field(validate_default=True)] = [ + {'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 index 3b58689e4..4e3a88330 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -18,6 +18,4 @@ class B(BaseModel): class Model(BaseModel): - x: A | B | None = Field( - default_factory=lambda: TypeAdapter(A | B).validate_python({'type': 'b'}) - ) + x: A | B | None = Field({'type': 'b'}, validate_default=True) 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 index 80eca5e3e..b7d49a576 100644 --- 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 @@ -7,7 +7,7 @@ from typing import Annotated, Literal from my_app import B -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -23,10 +23,5 @@ class Other(BaseModel): class Model(BaseModel): - x: Annotated[ - A | B1 | None, - Field( - default_factory=lambda: TypeAdapter(A | B1).validate_python({'type': 'b'}) - ), - ] + x: Annotated[A | B1 | None, Field(validate_default=True)] = {'type': 'b'} other: Other | None = None 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 index e083b4d74..8b447dbac 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Annotated, Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -18,9 +18,4 @@ class B(BaseModel): class Model(BaseModel): - x_value: Annotated[ - A | B | None, - Field( - default_factory=lambda: TypeAdapter(A | B).validate_python({'type': 'b'}) - ), - ] + x_value: Annotated[A | B | None, Field(validate_default=True)] = {'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 index 7916410d0..f7e54aa44 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Annotated, Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -19,11 +19,7 @@ class B(BaseModel): class Model(BaseModel): - x_value: Annotated[ - A | B | None, - Field( - default_factory=lambda: TypeAdapter(A | B).validate_python( - {'type': 'b', 'value': 1} - ) - ), - ] + x_value: Annotated[A | B | None, Field(validate_default=True)] = { + 'type': 'b', + 'value': 1, + } 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 index 6f5addbd2..58dd22b7a 100644 --- 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 @@ -7,7 +7,7 @@ from typing import Annotated, Literal from my_app import AliasA -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class B(BaseModel): @@ -15,11 +15,4 @@ class B(BaseModel): class Model(BaseModel): - x: Annotated[ - AliasA | B | None, - Field( - default_factory=lambda: TypeAdapter(AliasA | B).validate_python( - {'type': 'b'} - ) - ), - ] + x: Annotated[AliasA | B | None, Field(validate_default=True)] = {'type': 'b'} 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 index 3f2807a7a..fbae1d470 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -17,6 +17,4 @@ class A(BaseModel): class Model(BaseModel): - x: Alias | None = Field( - default_factory=lambda: TypeAdapter(Alias).validate_python([{'type': 'a'}]) - ) + x: Alias | None = Field([{'type': 'a'}], validate_default=True) 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 index c0075a848..430e27ddf 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Annotated, Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -22,11 +22,6 @@ class B(BaseModel): class Model(BaseModel): - x_value: Annotated[ - Alias | None, - Field( - default_factory=lambda: TypeAdapter(Alias).validate_python( - [{'type': 'b', 'value': 1}] - ) - ), + x_value: Annotated[Alias | None, Field(validate_default=True)] = [ + {'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 index 455bb9a0c..1ea2d9f5d 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -17,6 +17,4 @@ class A(BaseModel): class Model(BaseModel): - x: Alias | None = Field( - default_factory=lambda: TypeAdapter(Alias).validate_python({'type': 'a'}) - ) + x: Alias | None = Field({'type': 'a'}, validate_default=True) 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 index 5ac27b764..f469451e9 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -17,6 +17,4 @@ class A(BaseModel): class Model(BaseModel): - x: Alias | None = Field( - default_factory=lambda: TypeAdapter(Alias).validate_python([{'type': 'a'}]) - ) + x: Alias | None = Field([{'type': 'a'}], validate_default=True) 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 index b2d51ed47..d54d2aa1b 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -17,6 +17,4 @@ class A(BaseModel): class Model(BaseModel): - x: Alias | None = Field( - default_factory=lambda: TypeAdapter(Alias).validate_python({'type': 'a'}) - ) + x: Alias | None = Field({'type': 'a'}, validate_default=True) 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 index a4d6de172..4e657cac8 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -21,6 +21,4 @@ class B(BaseModel): class Model(BaseModel): - x: U | None = Field( - default_factory=lambda: TypeAdapter(U).validate_python({'type': 'b'}) - ) + x: U | None = Field({'type': 'b'}, validate_default=True) 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 index 9e66ffc07..b2583b77c 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Annotated, Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -21,7 +21,4 @@ class B(BaseModel): class Model(BaseModel): - x_value: Annotated[ - U | None, - Field(default_factory=lambda: TypeAdapter(U).validate_python({'type': 'b'})), - ] + x_value: Annotated[U | None, Field(validate_default=True)] = {'type': 'b'} 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 index 38faef29d..df089bba7 100644 --- 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 @@ -4,9 +4,9 @@ from __future__ import annotations -from typing import Literal +from typing import Annotated, Literal -from pydantic import BaseModel +from pydantic import BaseModel, Field class A(BaseModel): @@ -20,4 +20,4 @@ class A(BaseModel): class Model(BaseModel): - x: U | None = {'type': 'a'} + x: Annotated[U | None, Field(validate_default=True)] = {'type': 'a'} 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 index cda85e451..4f88d6402 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Annotated, Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class B(BaseModel): @@ -18,11 +18,7 @@ class B(BaseModel): class Model(BaseModel): - x_value: Annotated[ - Alias | None, - Field( - default_factory=lambda: TypeAdapter(Alias).validate_python( - {'type': 'b', 'value': 1} - ) - ), - ] + x_value: Annotated[Alias | None, Field(validate_default=True)] = { + '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 index 4e8800298..0d56a6af9 100644 --- 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 @@ -6,7 +6,7 @@ from typing import Annotated, Literal -from pydantic import BaseModel, Field, TypeAdapter +from pydantic import BaseModel, Field class A(BaseModel): @@ -21,7 +21,4 @@ class B(BaseModel): class Model(BaseModel): - x_value: Annotated[ - U | None, - Field(default_factory=lambda: TypeAdapter(U).validate_python({'type': 'b'})), - ] + x_value: Annotated[U | None, Field(validate_default=True)] = {'type': 'b'} diff --git a/tests/data/expected/main/jsonschema/without_titles_use_title_as_name.py b/tests/data/expected/main/jsonschema/without_titles_use_title_as_name.py index 172f9217f..53ec20d31 100644 --- a/tests/data/expected/main/jsonschema/without_titles_use_title_as_name.py +++ b/tests/data/expected/main/jsonschema/without_titles_use_title_as_name.py @@ -31,7 +31,7 @@ class ProcessingStatusUnion(BaseModel): class ProcessingTask(BaseModel): processing_status_union: ( ProcessingStatusUnion | ExtendedProcessingTask | ProcessingStatus | None - ) = Field(default_factory=lambda: ExtendedProcessingTask('COMPLETED')) + ) = Field('COMPLETED', validate_default=True) processing_status: ProcessingStatus | None = 'COMPLETED' name: str | None = None kind: Kind | None = None diff --git a/tests/data/expected/main/openapi/default_values_parameters_use_default.py b/tests/data/expected/main/openapi/default_values_parameters_use_default.py index 67796e68b..04a43ac6e 100644 --- a/tests/data/expected/main/openapi/default_values_parameters_use_default.py +++ b/tests/data/expected/main/openapi/default_values_parameters_use_default.py @@ -13,7 +13,7 @@ class Filter(BaseModel): class UsersGetParametersQuery(BaseModel): status: str | None = 'active' - filter: Filter | None = Field(default_factory=lambda: Filter.model_validate({})) + filter: Filter | None = Field({}, validate_default=True) class User(BaseModel): diff --git a/tests/data/expected/main/openapi/enum_models/one.py b/tests/data/expected/main/openapi/enum_models/one.py index 920994369..31633b361 100644 --- a/tests/data/expected/main/openapi/enum_models/one.py +++ b/tests/data/expected/main/openapi/enum_models/one.py @@ -101,9 +101,7 @@ class NestedVersion(RootModel[NestedVersionEnum | None]): class NestedNullableEnum(BaseModel): nested_version: NestedVersion | None = Field( - default_factory=lambda: NestedVersion('RC1'), - description='nullable enum', - examples=['RC2'], + 'RC1', description='nullable enum', examples=['RC2'], validate_default=True ) diff --git a/tests/data/expected/main/openapi/enum_models/one_literal_as_default.py b/tests/data/expected/main/openapi/enum_models/one_literal_as_default.py index fa9df2304..28e67bf2f 100644 --- a/tests/data/expected/main/openapi/enum_models/one_literal_as_default.py +++ b/tests/data/expected/main/openapi/enum_models/one_literal_as_default.py @@ -101,9 +101,7 @@ class NestedVersion(RootModel[NestedVersionEnum | None]): class NestedNullableEnum(BaseModel): nested_version: NestedVersion | None = Field( - default_factory=lambda: NestedVersion('RC1'), - description='nullable enum', - examples=['RC2'], + 'RC1', description='nullable enum', examples=['RC2'], validate_default=True ) diff --git a/tests/data/expected/main/openapi/pydantic_v2_default_object/Another.py b/tests/data/expected/main/openapi/pydantic_v2_default_object/Another.py index d74012238..9890bb9fe 100644 --- a/tests/data/expected/main/openapi/pydantic_v2_default_object/Another.py +++ b/tests/data/expected/main/openapi/pydantic_v2_default_object/Another.py @@ -16,8 +16,6 @@ class Foo(RootModel[str]): class Bar(BaseModel): original_foo: Foo_1 | None = Field( - default_factory=lambda: Foo_1.model_validate({'text': 'abc', 'number': 123}) - ) - nested_foo: list[Nested.Foo] | None = Field( - default_factory=lambda: [Nested.Foo.model_validate(v) for v in ['abc', 'efg']] + {'text': 'abc', 'number': 123}, validate_default=True ) + nested_foo: list[Nested.Foo] | None = Field(['abc', 'efg'], validate_default=True) diff --git a/tests/data/expected/main/openapi/pydantic_v2_default_object/Nested.py b/tests/data/expected/main/openapi/pydantic_v2_default_object/Nested.py index f952a7d7e..7a9e7e21c 100644 --- a/tests/data/expected/main/openapi/pydantic_v2_default_object/Nested.py +++ b/tests/data/expected/main/openapi/pydantic_v2_default_object/Nested.py @@ -14,13 +14,9 @@ class Foo(RootModel[str]): class Bar(BaseModel): - foo: Foo_1 | None = Field( - default_factory=lambda: Foo_1.model_validate({'text': 'abc', 'number': 123}) - ) + foo: Foo_1 | None = Field({'text': 'abc', 'number': 123}, validate_default=True) baz: list[Foo_1] | None = Field( - default_factory=lambda: [ - Foo_1.model_validate(v) - for v in [{'text': 'abc', 'number': 123}, {'text': 'efg', 'number': 456}] - ] + [{'text': 'abc', 'number': 123}, {'text': 'efg', 'number': 456}], + validate_default=True, ) - nested_foo: Foo | None = Field(default_factory=lambda: Foo('default foo')) + nested_foo: Foo | None = Field('default foo', validate_default=True) diff --git a/tests/data/expected/main/openapi/pydantic_v2_default_object/__init__.py b/tests/data/expected/main/openapi/pydantic_v2_default_object/__init__.py index 80b49581e..9373bda8c 100644 --- a/tests/data/expected/main/openapi/pydantic_v2_default_object/__init__.py +++ b/tests/data/expected/main/openapi/pydantic_v2_default_object/__init__.py @@ -13,12 +13,8 @@ class Foo(BaseModel): class Bar(BaseModel): - foo: Foo | None = Field( - default_factory=lambda: Foo.model_validate({'text': 'abc', 'number': 123}) - ) + foo: Foo | None = Field({'text': 'abc', 'number': 123}, validate_default=True) baz: list[Foo] | None = Field( - default_factory=lambda: [ - Foo.model_validate(v) - for v in [{'text': 'abc', 'number': 123}, {'text': 'efg', 'number': 456}] - ] + [{'text': 'abc', 'number': 123}, {'text': 'efg', 'number': 456}], + validate_default=True, ) 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 4678aeb8d..d52b65d9d 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 @@ -12,4 +12,4 @@ class Container(BaseModel): class PodSpec(BaseModel): - containers: list[Container] | None = Field(default_factory=list) + containers: list[Container] | None = Field([], validate_default=True) diff --git a/tests/data/expected/main/openapi/referenced_default.py b/tests/data/expected/main/openapi/referenced_default.py index 04ce050c6..91312a488 100644 --- a/tests/data/expected/main/openapi/referenced_default.py +++ b/tests/data/expected/main/openapi/referenced_default.py @@ -13,4 +13,4 @@ class ModelSettingB(RootModel[confloat(ge=0.0, le=10.0)]): class Model(BaseModel): settingA: confloat(ge=0.0, le=10.0) | None = 5 - settingB: ModelSettingB | None = Field(default_factory=lambda: ModelSettingB(5)) + settingB: ModelSettingB | None = Field(5, validate_default=True) diff --git a/tests/data/expected/main/openapi/root_model_default_primitive.py b/tests/data/expected/main/openapi/root_model_default_primitive.py index 063dc91df..2b3543741 100644 --- a/tests/data/expected/main/openapi/root_model_default_primitive.py +++ b/tests/data/expected/main/openapi/root_model_default_primitive.py @@ -12,4 +12,4 @@ class Timeout(RootModel[conint(le=14400, gt=0)]): class CrawlConfiguration(BaseModel): - timeout: Timeout | None = Field(default_factory=lambda: Timeout(3600)) + timeout: Timeout | None = Field(3600, validate_default=True) diff --git a/tests/data/expected/parser/openapi/openapi_parser_parse_enum_models/output.py b/tests/data/expected/parser/openapi/openapi_parser_parse_enum_models/output.py index 5d66ad61d..41a0719bf 100644 --- a/tests/data/expected/parser/openapi/openapi_parser_parse_enum_models/output.py +++ b/tests/data/expected/parser/openapi/openapi_parser_parse_enum_models/output.py @@ -117,9 +117,7 @@ class NestedVersion(RootModel[Optional[NestedVersionEnum]]): class NestedNullableEnum(BaseModel): nested_version: Optional[NestedVersion] = Field( - default_factory=lambda: NestedVersion('RC1'), - description='nullable enum', - examples=['RC2'], + 'RC1', description='nullable enum', examples=['RC2'], validate_default=True ) diff --git a/tests/parser/test_base.py b/tests/parser/test_base.py index 1284c8875..603fb7795 100644 --- a/tests/parser/test_base.py +++ b/tests/parser/test_base.py @@ -18,8 +18,7 @@ from datamodel_code_generator.parser.base import ( Parser, _contains_model_reference, - _get_validated_default_type_name, - _needs_validated_default, + _needs_validate_default, _unwrap_type_alias, add_model_path_to_list, escape_characters, @@ -690,15 +689,8 @@ def test_contains_model_reference_traverses_dict_key() -> None: 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) - - 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.""" +def test_needs_validate_default_for_union_type_alias() -> None: + """Test _needs_validate_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) @@ -711,11 +703,11 @@ def test_needs_validated_default_for_union_type_alias() -> None: 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 + assert _needs_validate_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.""" +def test_needs_validate_default_for_optional_single_model_union() -> None: + """Test _needs_validate_default returns True for A | None with dict default.""" model_reference = Reference(path="A", original_name="A", name="A") BaseModel(fields=[], reference=model_reference) @@ -724,4 +716,4 @@ def test_needs_validated_default_skips_optional_single_model_union() -> None: use_union_operator=True, ) - assert _needs_validated_default({"type": "a"}, optional_model_union) is False + assert _needs_validate_default({"type": "a"}, optional_model_union) is True From 95e28fa88795d189607999de7ccf94794174800f Mon Sep 17 00:00:00 2001 From: Mikhail Butvin Date: Fri, 13 Mar 2026 10:53:18 +0300 Subject: [PATCH 2/6] 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 --- src/datamodel_code_generator/parser/base.py | 6 ++---- tests/parser/test_base.py | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 581468e2f..4cc341890 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -436,10 +436,8 @@ def _contains_model_reference(data_type: DataType) -> bool: return False -def _needs_validate_default(default: Any, data_type: DataType) -> bool: +def _needs_validate_default(data_type: DataType) -> bool: """Check if a field needs validate_default=True to coerce defaults into model instances.""" - if default is None: - return False resolved = _unwrap_type_alias(data_type) return _contains_model_reference(resolved) @@ -2211,7 +2209,7 @@ def __set_validate_default_on_fields( # noqa: PLR6301 continue if isinstance(model_field.default, (Member, WrappedDefault)): continue - if not _needs_validate_default(model_field.default, model_field.data_type): + if not _needs_validate_default(model_field.data_type): continue model_field.extras["validate_default"] = True diff --git a/tests/parser/test_base.py b/tests/parser/test_base.py index 603fb7795..d7456d46f 100644 --- a/tests/parser/test_base.py +++ b/tests/parser/test_base.py @@ -703,7 +703,7 @@ def test_needs_validate_default_for_union_type_alias() -> None: alias_reference = Reference(path="Alias", original_name="Alias", name="Alias") TypeStatement(fields=[DataModelField(data_type=union_data_type)], reference=alias_reference) - assert _needs_validate_default({"type": "b"}, DataType(reference=alias_reference)) is True + assert _needs_validate_default(DataType(reference=alias_reference)) is True def test_needs_validate_default_for_optional_single_model_union() -> None: @@ -716,4 +716,4 @@ def test_needs_validate_default_for_optional_single_model_union() -> None: use_union_operator=True, ) - assert _needs_validate_default({"type": "a"}, optional_model_union) is True + assert _needs_validate_default(optional_model_union) is True From 20e5f8fc998c994396dd07e9c5adac2b8565e8d1 Mon Sep 17 00:00:00 2001 From: Mikhail Butvin Date: Fri, 13 Mar 2026 15:12:12 +0300 Subject: [PATCH 3/6] 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 --- src/datamodel_code_generator/model/_types.py | 18 ----------- src/datamodel_code_generator/model/base.py | 4 --- .../model/pydantic_base.py | 12 ++----- .../model/pydantic_v2/base_model.py | 1 - src/datamodel_code_generator/parser/base.py | 31 +------------------ .../jsonschema/root_model_default_value.py | 6 ++-- .../root_model_default_value_branches.py | 4 +-- .../root_model_default_value_non_root.py | 4 +-- .../referenced_default_use_annotated.py | 4 +-- 9 files changed, 9 insertions(+), 75 deletions(-) delete mode 100644 src/datamodel_code_generator/model/_types.py diff --git a/src/datamodel_code_generator/model/_types.py b/src/datamodel_code_generator/model/_types.py deleted file mode 100644 index e2b96fecf..000000000 --- a/src/datamodel_code_generator/model/_types.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Internal types for model module.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any - - -@dataclass(repr=False) -class WrappedDefault: - """Represents a default value wrapped with its type constructor.""" - - value: Any - type_name: str - - def __repr__(self) -> str: - """Return type constructor representation, e.g., 'CountType(10)'.""" - return f"{self.type_name}({self.value!r})" diff --git a/src/datamodel_code_generator/model/base.py b/src/datamodel_code_generator/model/base.py index 711e1aeb1..7110f58bc 100644 --- a/src/datamodel_code_generator/model/base.py +++ b/src/datamodel_code_generator/model/base.py @@ -25,7 +25,6 @@ IMPORT_UNION, Import, ) -from datamodel_code_generator.model._types import WrappedDefault from datamodel_code_generator.reference import Reference, _BaseModel from datamodel_code_generator.types import ( ANY, @@ -38,8 +37,6 @@ get_optional_type, ) -__all__ = ["WrappedDefault"] - if TYPE_CHECKING: from collections.abc import Iterator @@ -598,7 +595,6 @@ class DataModel(TemplateBase, Nullable, ABC): # noqa: PLR0904 SUPPORTS_GENERIC_BASE_CLASS: ClassVar[bool] = True SUPPORTS_DISCRIMINATOR: ClassVar[bool] = False SUPPORTS_FIELD_RENAMING: ClassVar[bool] = False - SUPPORTS_WRAPPED_DEFAULT: ClassVar[bool] = False SUPPORTS_KW_ONLY: ClassVar[bool] = False REQUIRES_RUNTIME_IMPORTS_WITH_RUFF_CHECK: 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 189f0146b..ccca66c20 100644 --- a/src/datamodel_code_generator/model/pydantic_base.py +++ b/src/datamodel_code_generator/model/pydantic_base.py @@ -19,7 +19,6 @@ DataModel, DataModelFieldBase, ) -from datamodel_code_generator.model._types import WrappedDefault from datamodel_code_generator.model.base import UNDEFINED, repr_set_sorted from datamodel_code_generator.types import UnionIntFloat, chain_as_tuple @@ -125,11 +124,6 @@ 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: - if isinstance(self.default, WrappedDefault): - return f"lambda :{self.default!r}" - return None - def _get_default_factory_for_optional_nested_model(self) -> str | None: """Get default_factory for optional nested Pydantic model fields. @@ -188,10 +182,10 @@ def __str__(self) -> str: # noqa: PLR0912 elif isinstance(discriminator, dict): # pragma: no cover data["discriminator"] = discriminator["propertyName"] - if self.required and not self.has_default: + if (self.required and not self.has_default) or ( + self.default is not UNDEFINED and self.default is not None and "default_factory" not in data + ): 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() else: default_factory = data.pop("default_factory", None) 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..f9b71c3ff 100644 --- a/src/datamodel_code_generator/model/pydantic_v2/base_model.py +++ b/src/datamodel_code_generator/model/pydantic_v2/base_model.py @@ -222,7 +222,6 @@ class BaseModel(BaseModelBase): BASE_CLASS_ALIAS: ClassVar[str] = "_BaseModel" SUPPORTS_DISCRIMINATOR: ClassVar[bool] = True SUPPORTS_FIELD_RENAMING: ClassVar[bool] = True - SUPPORTS_WRAPPED_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 4cc341890..a89a62a92 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -75,7 +75,6 @@ ConstraintsBase, DataModel, DataModelFieldBase, - WrappedDefault, ) from datamodel_code_generator.model.enum import Enum, Member from datamodel_code_generator.model.type_alias import TypeAliasBase, TypeStatement @@ -2169,33 +2168,6 @@ def __set_default_enum_member( else: enum_member.alias = data_type.alias - def __wrap_root_model_default_values( - self, - models: list[DataModel], - ) -> None: - """Wrap RootModel reference default values with their type constructors.""" - if not self.use_annotated or not self.data_model_type.SUPPORTS_WRAPPED_DEFAULT: - return - for model, model_field, data_type in iter_models_field_data_types(models): - if isinstance(model, (Enum, self.data_model_root_type)): - continue - if model_field.default is None: - continue - if isinstance(model_field.default, (WrappedDefault, Member)): - continue - if isinstance(model_field.default, list): - continue - if ( - data_type.reference - and isinstance(data_type.reference.source, self.data_model_root_type) - and not isinstance(data_type.reference.source, TypeAliasBase) - ): - type_name = data_type.alias or data_type.reference.short_name - model_field.default = WrappedDefault( - value=model_field.default, - type_name=type_name, - ) - def __set_validate_default_on_fields( # noqa: PLR6301 self, models: list[DataModel], @@ -2207,7 +2179,7 @@ def __set_validate_default_on_fields( # noqa: PLR6301 for model_field in model.fields: if model_field.default is None or model_field.default is UNDEFINED: continue - if isinstance(model_field.default, (Member, WrappedDefault)): + if isinstance(model_field.default, Member): continue if not _needs_validate_default(model_field.data_type): continue @@ -3248,7 +3220,6 @@ def _process_single_module( # noqa: PLR0913, PLR0917 self.__reuse_model(models, require_update_action_models) 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.__sort_models(models, imports, use_deferred_annotations=config.use_deferred_annotations) self.__change_field_name(models) self.__apply_discriminator_type(models, imports) diff --git a/tests/data/expected/main/jsonschema/root_model_default_value.py b/tests/data/expected/main/jsonschema/root_model_default_value.py index 7eae0e095..31f5809bc 100644 --- a/tests/data/expected/main/jsonschema/root_model_default_value.py +++ b/tests/data/expected/main/jsonschema/root_model_default_value.py @@ -25,7 +25,5 @@ class NameType(RootModel[str]): class Model(BaseModel): admin_state: AdminStateLeaf | None = AdminStateLeaf.enable - count: Annotated[CountType | None, Field(default_factory=lambda: CountType(10))] - name: Annotated[ - NameType | None, Field(default_factory=lambda: NameType('default_name')) - ] + count: Annotated[CountType | None, Field(validate_default=True)] = 10 + name: Annotated[NameType | None, Field(validate_default=True)] = 'default_name' diff --git a/tests/data/expected/main/jsonschema/root_model_default_value_branches.py b/tests/data/expected/main/jsonschema/root_model_default_value_branches.py index c22d7a048..fd21290ae 100644 --- a/tests/data/expected/main/jsonschema/root_model_default_value_branches.py +++ b/tests/data/expected/main/jsonschema/root_model_default_value_branches.py @@ -14,9 +14,7 @@ class CountType(RootModel[int]): class Model(BaseModel): - count_with_default: Annotated[ - CountType | None, Field(default_factory=lambda: CountType(10)) - ] + count_with_default: Annotated[CountType | None, Field(validate_default=True)] = 10 count_no_default: CountType | None = None count_list_default: Annotated[ list[CountType] | None, Field(validate_default=True) diff --git a/tests/data/expected/main/jsonschema/root_model_default_value_non_root.py b/tests/data/expected/main/jsonschema/root_model_default_value_non_root.py index f3753d86b..9f5b8b50e 100644 --- a/tests/data/expected/main/jsonschema/root_model_default_value_non_root.py +++ b/tests/data/expected/main/jsonschema/root_model_default_value_non_root.py @@ -18,9 +18,7 @@ class PersonType(BaseModel): class Model(BaseModel): - root_model_field: Annotated[ - CountType | None, Field(default_factory=lambda: CountType(10)) - ] + root_model_field: Annotated[CountType | None, Field(validate_default=True)] = 10 non_root_model_field: Annotated[PersonType | None, Field(validate_default=True)] = { 'name': 'John' } diff --git a/tests/data/expected/main/openapi/referenced_default_use_annotated.py b/tests/data/expected/main/openapi/referenced_default_use_annotated.py index 05670f70e..50d7d97bc 100644 --- a/tests/data/expected/main/openapi/referenced_default_use_annotated.py +++ b/tests/data/expected/main/openapi/referenced_default_use_annotated.py @@ -15,6 +15,4 @@ class ModelSettingB(RootModel[float]): class Model(BaseModel): settingA: Annotated[float | None, Field(ge=0.0, le=10.0)] = 5 - settingB: Annotated[ - ModelSettingB | None, Field(default_factory=lambda: ModelSettingB(5)) - ] + settingB: Annotated[ModelSettingB | None, Field(validate_default=True)] = 5 From eb5f86795378c8b79a0afc7f33af31325a03d747 Mon Sep 17 00:00:00 2001 From: Mikhail Butvin Date: Fri, 13 Mar 2026 15:54:00 +0300 Subject: [PATCH 4/6] 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 --- src/datamodel_code_generator/model/scalar.py | 5 +++-- .../main/graphql/default_values_required_use_default.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/datamodel_code_generator/model/scalar.py b/src/datamodel_code_generator/model/scalar.py index 89a8ee7ae..937894239 100644 --- a/src/datamodel_code_generator/model/scalar.py +++ b/src/datamodel_code_generator/model/scalar.py @@ -13,12 +13,13 @@ IMPORT_TYPE_ALIAS_TYPE, Import, ) -from datamodel_code_generator.model import DataModel, DataModelFieldBase from datamodel_code_generator.model.base import UNDEFINED +from datamodel_code_generator.model.type_alias import TypeAliasBase if TYPE_CHECKING: from pathlib import Path + from datamodel_code_generator.model import DataModelFieldBase from datamodel_code_generator.reference import Reference _INT: str = "int" @@ -38,7 +39,7 @@ } -class _DataTypeScalarBase(DataModel): +class _DataTypeScalarBase(TypeAliasBase): """Base class for GraphQL scalar types with shared __init__ logic.""" def __init__( # noqa: PLR0913 diff --git a/tests/data/expected/main/graphql/default_values_required_use_default.py b/tests/data/expected/main/graphql/default_values_required_use_default.py index 9992067ae..e9fed1bf3 100644 --- a/tests/data/expected/main/graphql/default_values_required_use_default.py +++ b/tests/data/expected/main/graphql/default_values_required_use_default.py @@ -29,6 +29,6 @@ class User(BaseModel): id: ID - name: String | None = Field('default_user', validate_default=True) - status: String | None = Field('active', validate_default=True) + name: String | None = 'default_user' + status: String | None = 'active' typename__: Literal['User'] | None = Field('User', alias='__typename') From c599a5a08d5d0a5883e9c16a3fad6872c8a3c209 Mon Sep 17 00:00:00 2001 From: Mikhail Butvin Date: Fri, 13 Mar 2026 16:24:07 +0300 Subject: [PATCH 5/6] 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 --- src/datamodel_code_generator/model/pydantic_base.py | 2 +- .../expected/main/graphql/pydantic_v2_empty_list_default.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/datamodel_code_generator/model/pydantic_base.py b/src/datamodel_code_generator/model/pydantic_base.py index ccca66c20..9c359afb7 100644 --- a/src/datamodel_code_generator/model/pydantic_base.py +++ b/src/datamodel_code_generator/model/pydantic_base.py @@ -211,7 +211,7 @@ def __str__(self) -> str: # noqa: PLR0912 if self.use_annotated: field_arguments = self._process_annotated_field_arguments(field_arguments) - elif self.required and not default_factory: + elif self.required and not default_factory and not self.extras.get("validate_default"): 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/pydantic_v2_empty_list_default.py b/tests/data/expected/main/graphql/pydantic_v2_empty_list_default.py index 1846955e3..052c98f4d 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 @@ -26,8 +26,8 @@ class Container(BaseModel): class PodSpec(BaseModel): - container_list: list[Container] = Field(..., validate_default=True) - container_list_or_none: list[Container | None] = Field(..., validate_default=True) + container_list: list[Container] = Field([], validate_default=True) + container_list_or_none: list[Container | None] = Field([], validate_default=True) container_or_none_list_or_none: list[Container | None] | None = Field( [], validate_default=True ) From 8e0874731691d22c59031e662e371c6a095e1bf3 Mon Sep 17 00:00:00 2001 From: Mikhail Butvin Date: Fri, 13 Mar 2026 17:03:38 +0300 Subject: [PATCH 6/6] Fix inaccurate docstring in test_needs_validate_default test Co-Authored-By: Claude Opus 4.6 --- tests/parser/test_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/parser/test_base.py b/tests/parser/test_base.py index d7456d46f..9163b5a1e 100644 --- a/tests/parser/test_base.py +++ b/tests/parser/test_base.py @@ -707,7 +707,7 @@ def test_needs_validate_default_for_union_type_alias() -> None: def test_needs_validate_default_for_optional_single_model_union() -> None: - """Test _needs_validate_default returns True for A | None with dict default.""" + """Test _needs_validate_default returns True for A | None union.""" model_reference = Reference(path="A", original_name="A", name="A") BaseModel(fields=[], reference=model_reference)