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..5399a9b30 100644 --- a/src/datamodel_code_generator/model/dataclass.py +++ b/src/datamodel_code_generator/model/dataclass.py @@ -172,7 +172,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..c929de75e 100644 --- a/src/datamodel_code_generator/model/msgspec.py +++ b/src/datamodel_code_generator/model/msgspec.py @@ -311,7 +311,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..5b60048fa 100644 --- a/src/datamodel_code_generator/model/pydantic_base.py +++ b/src/datamodel_code_generator/model/pydantic_base.py @@ -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 a89a62a92..81aee4263 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -2177,6 +2177,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): 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 3851be697..fc2a3d169 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -1201,6 +1201,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 @@ -1251,6 +1252,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: @@ -2477,22 +2479,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()) @@ -2828,12 +2830,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, @@ -2844,6 +2847,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_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/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_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/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index 40a397eb2..fd5b0e207 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -4753,6 +4753,28 @@ 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_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_main_root_one_of(output_dir: Path) -> None: """Test root-level oneOf schemas.""" run_main_and_assert(