diff --git a/src/datamodel_code_generator/model/base.py b/src/datamodel_code_generator/model/base.py index 7110f58bc..02eadbd61 100644 --- a/src/datamodel_code_generator/model/base.py +++ b/src/datamodel_code_generator/model/base.py @@ -173,6 +173,7 @@ class DataModelFieldBase(_BaseModel): use_frozen_field: bool = False use_serialization_alias: bool = False use_default_factory_for_optional_nested_models: bool = False + use_default_with_required: bool = False if not TYPE_CHECKING: # pragma: no branch diff --git a/src/datamodel_code_generator/model/dataclass.py b/src/datamodel_code_generator/model/dataclass.py index 00040d6d8..5fff58dbb 100644 --- a/src/datamodel_code_generator/model/dataclass.py +++ b/src/datamodel_code_generator/model/dataclass.py @@ -39,7 +39,8 @@ def has_field_assignment(field: DataModelFieldBase) -> bool: """Check if a dataclass field has a default value or field() assignment.""" return bool(field.field) or not ( - field.required or (field.represented_default == "None" and field.strip_default_none) + (field.required and not field.use_default_with_required) + or (field.represented_default == "None" and field.strip_default_none) ) @@ -172,7 +173,7 @@ def __str__(self) -> str: if self.default != UNDEFINED and self.default is not None: data["default"] = self.default - if self.required: + if self.required and not self.use_default_with_required: data = { k: v for k, v in data.items() diff --git a/src/datamodel_code_generator/model/msgspec.py b/src/datamodel_code_generator/model/msgspec.py index 2119f41d9..ba2cb78f7 100644 --- a/src/datamodel_code_generator/model/msgspec.py +++ b/src/datamodel_code_generator/model/msgspec.py @@ -70,7 +70,11 @@ def __str__(self) -> str: def _has_field_assignment(field: DataModelFieldBase) -> bool: - return not (field.required or (field.represented_default == "None" and field.strip_default_none)) + return ( + bool(field.field) + or field.use_default_with_required + or not (field.required or (field.represented_default == "None" and field.strip_default_none)) + ) DataModelFieldBaseT = TypeVar("DataModelFieldBaseT", bound=DataModelFieldBase) @@ -311,7 +315,7 @@ def __str__(self) -> str: # noqa: PLR0912 elif self._not_required and "default_factory" not in data: data["default"] = None if self.nullable else UNSET - if self.required: + if self.required and not self.use_default_with_required: data = { k: v for k, v in data.items() diff --git a/src/datamodel_code_generator/model/pydantic_base.py b/src/datamodel_code_generator/model/pydantic_base.py index 9c359afb7..998547d69 100644 --- a/src/datamodel_code_generator/model/pydantic_base.py +++ b/src/datamodel_code_generator/model/pydantic_base.py @@ -202,7 +202,7 @@ def __str__(self) -> str: # noqa: PLR0912 field_arguments = sorted(f"{k}={v!r}" for k, v in data.items() if v is not None) if not field_arguments and not default_factory: - if self.nullable and self.required: + if self.nullable and self.required and not self.use_default_with_required: return "Field(...)" # Field() is for mypy return "" @@ -211,7 +211,12 @@ 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 and not self.extras.get("validate_default"): + elif ( + self.required + and not self.use_default_with_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/src/datamodel_code_generator/model/template/dataclass.jinja2 b/src/datamodel_code_generator/model/template/dataclass.jinja2 index ecd31bffa..46b9ddab6 100644 --- a/src/datamodel_code_generator/model/template/dataclass.jinja2 +++ b/src/datamodel_code_generator/model/template/dataclass.jinja2 @@ -30,7 +30,7 @@ class {{ class_name }}: {{ field.name }}: {{ field.type_hint }} = {{ field.field }} {%- else %} {{ field.name }}: {{ field.type_hint }} - {%- if not (field.required or (field.represented_default == 'None' and field.strip_default_none)) + {%- if not ((field.required and not field.use_default_with_required) or (field.represented_default == 'None' and field.strip_default_none)) %} = {{ field.represented_default }} {%- endif -%} {%- endif %} diff --git a/src/datamodel_code_generator/model/template/msgspec.jinja2 b/src/datamodel_code_generator/model/template/msgspec.jinja2 index 6ab0540d2..0b712b297 100644 --- a/src/datamodel_code_generator/model/template/msgspec.jinja2 +++ b/src/datamodel_code_generator/model/template/msgspec.jinja2 @@ -29,7 +29,7 @@ class {{ class_name }}: {%- else %} {{ field.name }}: {{ field.type_hint }} {%- endif %} - {%- if not field.field and (not field.required or field.data_type.is_optional or field.nullable) + {%- if not field.field and (not field.required or field.use_default_with_required or field.data_type.is_optional or field.nullable) %} = {{ field.represented_default }} {%- endif -%} {%- endif %} diff --git a/src/datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 b/src/datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 index 828c4b416..0d812ff60 100644 --- a/src/datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 +++ b/src/datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 @@ -27,7 +27,7 @@ class {{ class_name }}({{ base_class }}):{% if comment is defined %} # {{ comme {%- else %} {{ field.name }}: {{ field.type_hint }} {%- endif %} - {%- if not field.has_default_factory_in_field and not field.required and (field.represented_default != 'None' or not field.strip_default_none or field.data_type.is_optional) + {%- if not field.has_default_factory_in_field and (not field.required or field.use_default_with_required) and (field.represented_default != 'None' or not field.strip_default_none or field.data_type.is_optional) %} = {{ field.represented_default }} {%- endif -%} {%- endif %} diff --git a/src/datamodel_code_generator/model/template/pydantic_v2/dataclass.jinja2 b/src/datamodel_code_generator/model/template/pydantic_v2/dataclass.jinja2 index 3fcc5ad83..00b12e628 100644 --- a/src/datamodel_code_generator/model/template/pydantic_v2/dataclass.jinja2 +++ b/src/datamodel_code_generator/model/template/pydantic_v2/dataclass.jinja2 @@ -41,7 +41,7 @@ class {{ class_name }}: {%- else %} {{ field.name }}: {{ field.type_hint }} {%- endif %} - {%- if not field.has_default_factory_in_field and not field.required and (field.represented_default != 'None' or not field.strip_default_none or field.data_type.is_optional) + {%- if not field.has_default_factory_in_field and (not field.required or field.use_default_with_required) and (field.represented_default != 'None' or not field.strip_default_none or field.data_type.is_optional) %} = {{ field.represented_default }} {%- endif -%} {%- endif %} diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index ecbad8e72..ada2399b3 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -2180,6 +2180,8 @@ def __set_validate_default_on_fields( # noqa: PLR6301 if isinstance(model, Enum): continue for model_field in model.fields: + if model_field.required and not model_field.use_default_with_required: + continue if model_field.default is None or model_field.default is UNDEFINED: continue if isinstance(model_field.default, Member): @@ -2227,6 +2229,8 @@ def __override_required_field( copied_original_field.data_type = data_type copied_original_field.parent = model copied_original_field.required = True + if self.apply_default_values_for_required_fields and copied_original_field.has_default: + copied_original_field.use_default_with_required = True model.fields.insert(index, copied_original_field) model.fields.remove(model_field) diff --git a/src/datamodel_code_generator/parser/graphql.py b/src/datamodel_code_generator/parser/graphql.py index 82f7e7051..be6184d1f 100644 --- a/src/datamodel_code_generator/parser/graphql.py +++ b/src/datamodel_code_generator/parser/graphql.py @@ -372,8 +372,7 @@ def parse_field( class_name=class_name, ) - if self.apply_default_values_for_required_fields and effective_has_default: - required = False + use_default_with_required = required and self.apply_default_values_for_required_fields and effective_has_default extras = {} if self.default_field_extras is None else self.default_field_extras.copy() @@ -405,6 +404,7 @@ def parse_field( original_name=field_name, has_default=effective_has_default, use_serialization_alias=self.use_serialization_alias, + use_default_with_required=use_default_with_required, ) def parse_object_like( diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 34ee95f3c..d02c0bfd7 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -1202,6 +1202,7 @@ def get_object_field( # noqa: PLR0913 original_field_name: str | None, effective_default: Any = None, effective_has_default: bool | None = None, + use_default_with_required: bool = False, ) -> DataModelFieldBase: """Create a data model field from a JSON Schema object field.""" default_value = effective_default if effective_has_default is not None else field.default @@ -1252,6 +1253,7 @@ def get_object_field( # noqa: PLR0913 use_frozen_field=self.use_frozen_field, use_serialization_alias=self.use_serialization_alias, use_default_factory_for_optional_nested_models=self.use_default_factory_for_optional_nested_models, + use_default_with_required=use_default_with_required, ) def get_data_type(self, obj: JsonSchemaObject) -> DataType: @@ -2478,22 +2480,22 @@ def _parse_object_common_part( # noqa: PLR0912, PLR0913, PLR0915 return self.data_type(reference=base_classes[0]) if required: for field in fields: - if self.force_optional_for_required_fields or ( # pragma: no cover - self.apply_default_values_for_required_fields and field.has_default - ): + if self.force_optional_for_required_fields: # pragma: no cover continue # pragma: no cover if (field.original_name or field.name) in required: field.required = True + if self.apply_default_values_for_required_fields and field.has_default: + field.use_default_with_required = True if obj.required: field_name_to_field = {f.original_name or f.name: f for f in fields} for required_ in obj.required: if required_ in field_name_to_field: field = field_name_to_field[required_] - if self.force_optional_for_required_fields or ( - self.apply_default_values_for_required_fields and field.has_default - ): + if self.force_optional_for_required_fields: continue field.required = True + if self.apply_default_values_for_required_fields and field.has_default: + field.use_default_with_required = True else: fields.append( self.data_model_field_type(required=True, original_name=required_, data_type=DataType()) @@ -2829,12 +2831,13 @@ def parse_object_fields( class_name=class_name, ) - if self.force_optional_for_required_fields or ( - self.apply_default_values_for_required_fields and effective_has_default - ): + if self.force_optional_for_required_fields: required: bool = False else: required = original_field_name in requires + use_default_with_required = ( + required and self.apply_default_values_for_required_fields and effective_has_default + ) fields.append( self.get_object_field( field_name=field_name, @@ -2845,6 +2848,7 @@ def parse_object_fields( original_field_name=original_field_name, effective_default=effective_default, effective_has_default=effective_has_default, + use_default_with_required=use_default_with_required, ) ) return fields diff --git a/src/datamodel_code_generator/parser/openapi.py b/src/datamodel_code_generator/parser/openapi.py index 2e30a46b1..5f327d218 100644 --- a/src/datamodel_code_generator/parser/openapi.py +++ b/src/datamodel_code_generator/parser/openapi.py @@ -508,7 +508,7 @@ def _get_model_name(cls, path_name: str, method: str, suffix: str) -> str: camel_path_name = snake_to_upper_camel(normalized) return f"{camel_path_name}{method.capitalize()}{suffix}" - def parse_all_parameters( # noqa: PLR0912, PLR0914, PLR0915 + def parse_all_parameters( # noqa: PLR0912, PLR0914 self, name: str, parameters: list[ReferenceObject | ParameterObject], @@ -551,8 +551,9 @@ def parse_all_parameters( # noqa: PLR0912, PLR0914, PLR0915 class_name=reference.name, ) effective_required = parameter.required - if self.apply_default_values_for_required_fields and effective_has_default: - effective_required = False + use_default_with_required = ( + effective_required and self.apply_default_values_for_required_fields and effective_has_default + ) fields.append( self.get_object_field( field_name=field_name, @@ -563,6 +564,7 @@ def parse_all_parameters( # noqa: PLR0912, PLR0914, PLR0915 alias=alias, effective_default=effective_default, effective_has_default=effective_has_default, + use_default_with_required=use_default_with_required, ) ) else: @@ -600,8 +602,9 @@ def parse_all_parameters( # noqa: PLR0912, PLR0914, PLR0915 class_name=reference.name, ) effective_required = parameter.required - if self.apply_default_values_for_required_fields and effective_has_default: - effective_required = False + use_default_with_required = ( + effective_required and self.apply_default_values_for_required_fields and effective_has_default + ) # Handle multiple aliases (Pydantic v2 AliasChoices) single_alias: str | None = None validation_aliases: list[str] | None = None @@ -639,6 +642,7 @@ def parse_all_parameters( # noqa: PLR0912, PLR0914, PLR0915 has_default=effective_has_default, type_has_null=object_schema.type_has_null if object_schema else None, use_serialization_alias=self.use_serialization_alias, + use_default_with_required=use_default_with_required, ) ) 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..6dc4c1b3b 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 = 'default_user' + status: String = 'active' 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 052c98f4d..1f90a7edd 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] + container_list_or_none: list[Container | None] container_or_none_list_or_none: list[Container | None] | None = Field( [], validate_default=True ) diff --git a/tests/data/expected/main/jsonschema/all_of_use_default.py b/tests/data/expected/main/jsonschema/all_of_use_default.py index e4f6603e6..3ba8692e4 100644 --- a/tests/data/expected/main/jsonschema/all_of_use_default.py +++ b/tests/data/expected/main/jsonschema/all_of_use_default.py @@ -8,5 +8,5 @@ class Item(BaseModel): - test: str | None = 'test123' - testarray: list[str] | None = Field(['test123'], min_length=1, title='test array') + test: str = 'test123' + testarray: list[str] = Field(['test123'], min_length=1, title='test array') diff --git a/tests/data/expected/main/jsonschema/allof_inherited_required_use_default.py b/tests/data/expected/main/jsonschema/allof_inherited_required_use_default.py new file mode 100644 index 000000000..2aa3eade5 --- /dev/null +++ b/tests/data/expected/main/jsonschema/allof_inherited_required_use_default.py @@ -0,0 +1,15 @@ +# generated by datamodel-codegen: +# filename: allof_inherited_required_use_default.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from pydantic import BaseModel + + +class Base(BaseModel): + id: int | None = 1 + + +class Container(Base): + id: int = 1 diff --git a/tests/data/expected/main/jsonschema/allof_required_use_default.py b/tests/data/expected/main/jsonschema/allof_required_use_default.py new file mode 100644 index 000000000..5202311f7 --- /dev/null +++ b/tests/data/expected/main/jsonschema/allof_required_use_default.py @@ -0,0 +1,16 @@ +# generated by datamodel-codegen: +# filename: allof_required_use_default.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from pydantic import BaseModel + + +class Base(BaseModel): + id: int + + +class Container(Base): + name: str = 'unnamed' + tag: str diff --git a/tests/data/expected/main/jsonschema/force_optional_required.py b/tests/data/expected/main/jsonschema/force_optional_required.py new file mode 100644 index 000000000..5cbb7b306 --- /dev/null +++ b/tests/data/expected/main/jsonschema/force_optional_required.py @@ -0,0 +1,16 @@ +# generated by datamodel-codegen: +# filename: force_optional_required.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from pydantic import BaseModel + + +class Base(BaseModel): + id: int | None = None + + +class Container(Base): + name: str | None = None + value: int | None = None diff --git a/tests/data/expected/main/jsonschema/use_default_msgspec_field_ordering.py b/tests/data/expected/main/jsonschema/use_default_msgspec_field_ordering.py new file mode 100644 index 000000000..39a44d1fc --- /dev/null +++ b/tests/data/expected/main/jsonschema/use_default_msgspec_field_ordering.py @@ -0,0 +1,16 @@ +# generated by datamodel-codegen: +# filename: allof_required_use_default.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from msgspec import Struct + + +class Base(Struct): + id: int + + +class Container(Base): + tag: str + name: str = 'unnamed' diff --git a/tests/data/expected/main/jsonschema/use_default_required_null_default.py b/tests/data/expected/main/jsonschema/use_default_required_null_default.py new file mode 100644 index 000000000..b0c6cbb74 --- /dev/null +++ b/tests/data/expected/main/jsonschema/use_default_required_null_default.py @@ -0,0 +1,13 @@ +# generated by datamodel-codegen: +# filename: use_default_required_null_default.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class Container: + b: int + a: str | None = None diff --git a/tests/data/expected/main/jsonschema/use_default_strict_nullable_required.py b/tests/data/expected/main/jsonschema/use_default_strict_nullable_required.py new file mode 100644 index 000000000..e99c216cc --- /dev/null +++ b/tests/data/expected/main/jsonschema/use_default_strict_nullable_required.py @@ -0,0 +1,11 @@ +# generated by datamodel-codegen: +# filename: use_default_strict_nullable_required.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from pydantic import BaseModel + + +class Container(BaseModel): + x: str | None = 'hello' 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 04a43ac6e..3b25fe17f 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 @@ -12,8 +12,8 @@ class Filter(BaseModel): class UsersGetParametersQuery(BaseModel): - status: str | None = 'active' - filter: Filter | None = Field({}, validate_default=True) + status: str = 'active' + filter: Filter = Field({}, validate_default=True) class User(BaseModel): diff --git a/tests/data/expected/main/openapi/use_default.py b/tests/data/expected/main/openapi/use_default.py index a1a76458e..d8f4c79aa 100644 --- a/tests/data/expected/main/openapi/use_default.py +++ b/tests/data/expected/main/openapi/use_default.py @@ -8,7 +8,7 @@ class Pet(BaseModel): - id: int | None = 1 + id: int = 1 name: str tag: str | None = None diff --git a/tests/data/jsonschema/allof_inherited_required_use_default.json b/tests/data/jsonschema/allof_inherited_required_use_default.json new file mode 100644 index 000000000..8be00d12a --- /dev/null +++ b/tests/data/jsonschema/allof_inherited_required_use_default.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Container", + "definitions": { + "Base": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "default": 1 + } + } + } + }, + "allOf": [ + { + "$ref": "#/definitions/Base" + } + ], + "required": ["id"] +} diff --git a/tests/data/jsonschema/allof_required_use_default.json b/tests/data/jsonschema/allof_required_use_default.json new file mode 100644 index 000000000..632a02b56 --- /dev/null +++ b/tests/data/jsonschema/allof_required_use_default.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Container", + "definitions": { + "Base": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + }, + "required": ["id"] + } + }, + "allOf": [ + {"$ref": "#/definitions/Base"}, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "default": "unnamed" + }, + "tag": { + "type": "string" + } + }, + "required": ["name", "tag"] + } + ] +} diff --git a/tests/data/jsonschema/force_optional_required.json b/tests/data/jsonschema/force_optional_required.json new file mode 100644 index 000000000..9e2838dc8 --- /dev/null +++ b/tests/data/jsonschema/force_optional_required.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Container", + "definitions": { + "Base": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + } + }, + "allOf": [ + {"$ref": "#/definitions/Base"}, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer" + } + } + } + ], + "required": ["name", "value"] +} diff --git a/tests/data/jsonschema/use_default_required_null_default.json b/tests/data/jsonschema/use_default_required_null_default.json new file mode 100644 index 000000000..5eef1a8c0 --- /dev/null +++ b/tests/data/jsonschema/use_default_required_null_default.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Container", + "type": "object", + "properties": { + "a": {"type": ["string", "null"], "default": null}, + "b": {"type": "integer"} + }, + "required": ["a", "b"] +} diff --git a/tests/data/jsonschema/use_default_strict_nullable_required.json b/tests/data/jsonschema/use_default_strict_nullable_required.json new file mode 100644 index 000000000..53db65065 --- /dev/null +++ b/tests/data/jsonschema/use_default_strict_nullable_required.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Container", + "type": "object", + "properties": { + "x": {"type": "string", "nullable": true, "default": "hello"} + }, + "required": ["x"] +} diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index 1ca62ec3d..e18f48209 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -510,19 +510,9 @@ def test_main_null_and_array(output_file: Path) -> None: The `--use-default` flag allows required fields with default values to be generated with their defaults, making them optional to provide when instantiating the model. -!!! warning "Fields with defaults become nullable" - When using `--use-default`, fields with default values are generated as nullable - types (e.g., `str | None` instead of `str`), even if the schema does not allow - null values. - - If you want fields to strictly follow the schema's type definition (non-nullable), - use `--strict-nullable` together with `--use-default`. - -!!! note "Future behavior change" - In a future major version, the default behavior of `--use-default` may change to - generate non-nullable types that match the schema definition (equivalent to using - `--strict-nullable`). If you rely on the current nullable behavior, consider - explicitly handling this in your code.""", + The field type still follows the schema's nullability. For example, a required + string field with a default is generated as `str = 'value'`, not + `str | None = 'value'`, unless the schema allows null.""", input_schema="jsonschema/use_default_with_const.json", cli_args=["--output-model-type", "pydantic_v2.BaseModel", "--use-default"], golden_output="jsonschema/use_default_with_const.py", @@ -534,19 +524,9 @@ def test_use_default_pydantic_v2_with_json_schema_const(output_file: Path) -> No The `--use-default` flag allows required fields with default values to be generated with their defaults, making them optional to provide when instantiating the model. - !!! warning "Fields with defaults become nullable" - When using `--use-default`, fields with default values are generated as nullable - types (e.g., `str | None` instead of `str`), even if the schema does not allow - null values. - - If you want fields to strictly follow the schema's type definition (non-nullable), - use `--strict-nullable` together with `--use-default`. - - !!! note "Future behavior change" - In a future major version, the default behavior of `--use-default` may change to - generate non-nullable types that match the schema definition (equivalent to using - `--strict-nullable`). If you rely on the current nullable behavior, consider - explicitly handling this in your code. + The field type still follows the schema's nullability. For example, a required + string field with a default is generated as `str = 'value'`, not + `str | None = 'value'`, unless the schema allows null. """ run_main_and_assert( input_path=JSON_SCHEMA_DATA_PATH / "use_default_with_const.json", @@ -4947,6 +4927,72 @@ def test_all_of_use_default(output_file: Path) -> None: ) +def test_allof_required_use_default(output_file: Path) -> None: + """Test allOf with required fields and --use-default renders defaults without nullable types.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "allof_required_use_default.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + extra_args=["--use-default"], + ) + + +def test_allof_inherited_required_use_default(output_file: Path) -> None: + """Test allOf required override preserves inherited defaults with --use-default.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "allof_inherited_required_use_default.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + extra_args=["--use-default"], + ) + + +def test_force_optional_required(output_file: Path) -> None: + """Test --force-optional makes required fields optional.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "force_optional_required.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + extra_args=["--force-optional"], + ) + + +def test_use_default_msgspec_field_ordering(output_file: Path) -> None: + """Required fields with defaults must sort after required fields without for msgspec.Struct.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "allof_required_use_default.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + extra_args=["--use-default", "--output-model-type", "msgspec.Struct"], + ) + + +def test_use_default_required_null_default(output_file: Path) -> None: + """Required nullable field with default null must not break dataclass field ordering.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "use_default_required_null_default.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + extra_args=["--use-default", "--output-model-type", "dataclasses.dataclass"], + ) + + +def test_use_default_strict_nullable_required(output_file: Path) -> None: + """Required nullable field with default must render the default, not Field(...), under --strict-nullable.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "use_default_strict_nullable_required.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + extra_args=["--use-default", "--strict-nullable", "--output-model-type", "pydantic_v2.BaseModel"], + ) + + def test_main_root_one_of(output_dir: Path) -> None: """Test root-level oneOf schemas.""" run_main_and_assert(