Skip to content

Commit 160e507

Browse files
butvinmclaudekoxudaxi
authored
Fix required field default rendering and --use-default nullable types (#3054)
* Fix required fields inconsistently rendering defaults (#3048) __set_validate_default_on_fields set validate_default=True on fields with model references without checking field.required, causing required model-ref fields to render defaults while required scalar fields didn't. Skip required fields (unless use_default_with_required) so all required fields consistently have no defaults rendered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix --use-default making required fields nullable --use-default previously skipped setting field.required=True, which made fields non-required and therefore nullable (e.g. str | None = 'foo'). Now fields stay required=True with a new use_default_with_required flag that allows defaults to render without changing the type. Produces `status: str = 'foo'` instead of `status: str | None = 'foo'`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add tests for allOf --use-default and --force-optional coverage Cover the previously uncovered code paths: - allOf with $ref + sub-schema required fields + --use-default - allOf with outer required fields + --force-optional Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Pass use_default_with_required via constructor instead of patching Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Propagate use_default_with_required to missed field-assignment sites Three regressions where the new use_default_with_required flag wasn't propagated to all the rendering sites that decide whether a field has an assignment. Each produces broken output on this branch but the correct output on baseline (92bdc27). 1. pydantic_base.py:205 — early-return for required+nullable fields with no other Field args returned Field(...) without checking use_default_with_required, dropping the user's default. Repro: --use-default --strict-nullable, required nullable str with default. Got: x: str | None = Field(...). Want: x: str | None = 'hello'. 2. dataclass.py:has_field_assignment — sort-key helper didn't account for use_default_with_required, so a required field with default null (use_default_with_required=True, field.field empty) was sorted as no-assignment alongside truly required fields. Schema order was then preserved, placing the defaulted field before the non-defaulted one. Repro: --use-default --output-model-type dataclasses.dataclass, required nullable a (default null) before required b. Got: a before b (TypeError on import). Want: b before a. Same fix covers pydantic_v2.dataclass since it shares the helper. 3. msgspec.py:_has_field_assignment — same class of bug. Helper checked neither bool(field.field) nor use_default_with_required, breaking ordering for both string-defaulted and null-defaulted required fields. Repro: --use-default --output-model-type msgspec.Struct on the existing allof_required_use_default.json fixture. Got: name='unnamed' before tag (TypeError on import). Want: tag before name='unnamed'. Adds three regression tests in tests/main/jsonschema/test_main_jsonschema.py covering the three repros above. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix required default handling --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Koudai Aono <koxudaxi@gmail.com>
1 parent ece1783 commit 160e507

29 files changed

Lines changed: 317 additions & 61 deletions

src/datamodel_code_generator/model/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ class DataModelFieldBase(_BaseModel):
173173
use_frozen_field: bool = False
174174
use_serialization_alias: bool = False
175175
use_default_factory_for_optional_nested_models: bool = False
176+
use_default_with_required: bool = False
176177

177178
if not TYPE_CHECKING: # pragma: no branch
178179

src/datamodel_code_generator/model/dataclass.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
def has_field_assignment(field: DataModelFieldBase) -> bool:
4040
"""Check if a dataclass field has a default value or field() assignment."""
4141
return bool(field.field) or not (
42-
field.required or (field.represented_default == "None" and field.strip_default_none)
42+
(field.required and not field.use_default_with_required)
43+
or (field.represented_default == "None" and field.strip_default_none)
4344
)
4445

4546

@@ -172,7 +173,7 @@ def __str__(self) -> str:
172173
if self.default != UNDEFINED and self.default is not None:
173174
data["default"] = self.default
174175

175-
if self.required:
176+
if self.required and not self.use_default_with_required:
176177
data = {
177178
k: v
178179
for k, v in data.items()

src/datamodel_code_generator/model/msgspec.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ def __str__(self) -> str:
7070

7171

7272
def _has_field_assignment(field: DataModelFieldBase) -> bool:
73-
return not (field.required or (field.represented_default == "None" and field.strip_default_none))
73+
return (
74+
bool(field.field)
75+
or field.use_default_with_required
76+
or not (field.required or (field.represented_default == "None" and field.strip_default_none))
77+
)
7478

7579

7680
DataModelFieldBaseT = TypeVar("DataModelFieldBaseT", bound=DataModelFieldBase)
@@ -311,7 +315,7 @@ def __str__(self) -> str: # noqa: PLR0912
311315
elif self._not_required and "default_factory" not in data:
312316
data["default"] = None if self.nullable else UNSET
313317

314-
if self.required:
318+
if self.required and not self.use_default_with_required:
315319
data = {
316320
k: v
317321
for k, v in data.items()

src/datamodel_code_generator/model/pydantic_base.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def __str__(self) -> str: # noqa: PLR0912
202202
field_arguments = sorted(f"{k}={v!r}" for k, v in data.items() if v is not None)
203203

204204
if not field_arguments and not default_factory:
205-
if self.nullable and self.required:
205+
if self.nullable and self.required and not self.use_default_with_required:
206206
return "Field(...)" # Field() is for mypy
207207
return ""
208208

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

212212
if self.use_annotated:
213213
field_arguments = self._process_annotated_field_arguments(field_arguments)
214-
elif self.required and not default_factory and not self.extras.get("validate_default"):
214+
elif (
215+
self.required
216+
and not self.use_default_with_required
217+
and not default_factory
218+
and not self.extras.get("validate_default")
219+
):
215220
field_arguments = ["...", *field_arguments]
216221
elif not default_factory:
217222
default_repr = repr_set_sorted(self.default) if isinstance(self.default, set) else repr(self.default)

src/datamodel_code_generator/model/template/dataclass.jinja2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class {{ class_name }}:
3030
{{ field.name }}: {{ field.type_hint }} = {{ field.field }}
3131
{%- else %}
3232
{{ field.name }}: {{ field.type_hint }}
33-
{%- if not (field.required or (field.represented_default == 'None' and field.strip_default_none))
33+
{%- if not ((field.required and not field.use_default_with_required) or (field.represented_default == 'None' and field.strip_default_none))
3434
%} = {{ field.represented_default }}
3535
{%- endif -%}
3636
{%- endif %}

src/datamodel_code_generator/model/template/msgspec.jinja2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class {{ class_name }}:
2929
{%- else %}
3030
{{ field.name }}: {{ field.type_hint }}
3131
{%- endif %}
32-
{%- if not field.field and (not field.required or field.data_type.is_optional or field.nullable)
32+
{%- if not field.field and (not field.required or field.use_default_with_required or field.data_type.is_optional or field.nullable)
3333
%} = {{ field.represented_default }}
3434
{%- endif -%}
3535
{%- endif %}

src/datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class {{ class_name }}({{ base_class }}):{% if comment is defined %} # {{ comme
2727
{%- else %}
2828
{{ field.name }}: {{ field.type_hint }}
2929
{%- endif %}
30-
{%- 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)
30+
{%- 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)
3131
%} = {{ field.represented_default }}
3232
{%- endif -%}
3333
{%- endif %}

src/datamodel_code_generator/model/template/pydantic_v2/dataclass.jinja2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class {{ class_name }}:
4141
{%- else %}
4242
{{ field.name }}: {{ field.type_hint }}
4343
{%- endif %}
44-
{%- 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)
44+
{%- 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)
4545
%} = {{ field.represented_default }}
4646
{%- endif -%}
4747
{%- endif %}

src/datamodel_code_generator/parser/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2180,6 +2180,8 @@ def __set_validate_default_on_fields( # noqa: PLR6301
21802180
if isinstance(model, Enum):
21812181
continue
21822182
for model_field in model.fields:
2183+
if model_field.required and not model_field.use_default_with_required:
2184+
continue
21832185
if model_field.default is None or model_field.default is UNDEFINED:
21842186
continue
21852187
if isinstance(model_field.default, Member):
@@ -2227,6 +2229,8 @@ def __override_required_field(
22272229
copied_original_field.data_type = data_type
22282230
copied_original_field.parent = model
22292231
copied_original_field.required = True
2232+
if self.apply_default_values_for_required_fields and copied_original_field.has_default:
2233+
copied_original_field.use_default_with_required = True
22302234
model.fields.insert(index, copied_original_field)
22312235
model.fields.remove(model_field)
22322236

src/datamodel_code_generator/parser/graphql.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,7 @@ def parse_field(
372372
class_name=class_name,
373373
)
374374

375-
if self.apply_default_values_for_required_fields and effective_has_default:
376-
required = False
375+
use_default_with_required = required and self.apply_default_values_for_required_fields and effective_has_default
377376

378377
extras = {} if self.default_field_extras is None else self.default_field_extras.copy()
379378

@@ -405,6 +404,7 @@ def parse_field(
405404
original_name=field_name,
406405
has_default=effective_has_default,
407406
use_serialization_alias=self.use_serialization_alias,
407+
use_default_with_required=use_default_with_required,
408408
)
409409

410410
def parse_object_like(

0 commit comments

Comments
 (0)