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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/datamodel_code_generator/model/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/model/dataclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/model/msgspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 6 additions & 1 deletion src/datamodel_code_generator/model/pydantic_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/model/template/msgspec.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
2 changes: 2 additions & 0 deletions src/datamodel_code_generator/parser/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions src/datamodel_code_generator/parser/graphql.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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(
Expand Down
22 changes: 13 additions & 9 deletions src/datamodel_code_generator/parser/jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
14 changes: 9 additions & 5 deletions src/datamodel_code_generator/parser/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
4 changes: 2 additions & 2 deletions tests/data/expected/main/jsonschema/all_of_use_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
16 changes: 16 additions & 0 deletions tests/data/expected/main/jsonschema/allof_required_use_default.py
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions tests/data/expected/main/jsonschema/force_optional_required.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/data/expected/main/openapi/use_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


class Pet(BaseModel):
id: int | None = 1
id: int = 1
name: str
tag: str | None = None

Expand Down
31 changes: 31 additions & 0 deletions tests/data/jsonschema/allof_required_use_default.json
Original file line number Diff line number Diff line change
@@ -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"]
}
]
}
29 changes: 29 additions & 0 deletions tests/data/jsonschema/force_optional_required.json
Original file line number Diff line number Diff line change
@@ -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"]
}
22 changes: 22 additions & 0 deletions tests/main/jsonschema/test_main_jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading