Skip to content

Commit dc47a97

Browse files
butvinmclaude
andcommitted
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>
1 parent 84ca7a4 commit dc47a97

15 files changed

Lines changed: 91 additions & 83 deletions

File tree

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def __str__(self) -> str:
172172
if self.default != UNDEFINED and self.default is not None:
173173
data["default"] = self.default
174174

175-
if self.required:
175+
if self.required and not self.use_default_with_required:
176176
data = {
177177
k: v
178178
for k, v in data.items()

src/datamodel_code_generator/model/msgspec.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ def __str__(self) -> str: # noqa: PLR0912
311311
elif self._not_required and "default_factory" not in data:
312312
data["default"] = None if self.nullable else UNSET
313313

314-
if self.required:
314+
if self.required and not self.use_default_with_required:
315315
data = {
316316
k: v
317317
for k, v in data.items()

src/datamodel_code_generator/model/pydantic_base.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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/graphql.py

Lines changed: 4 additions & 3 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

@@ -387,7 +386,7 @@ def parse_field(
387386
validation_aliases = alias
388387
else:
389388
single_alias = alias
390-
return self.data_model_field_type(
389+
new_field = self.data_model_field_type(
391390
name=field_name,
392391
default=effective_default,
393392
data_type=final_data_type,
@@ -406,6 +405,8 @@ def parse_field(
406405
has_default=effective_has_default,
407406
use_serialization_alias=self.use_serialization_alias,
408407
)
408+
new_field.use_default_with_required = use_default_with_required
409+
return new_field
409410

410411
def parse_object_like(
411412
self,

src/datamodel_code_generator/parser/jsonschema.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2477,22 +2477,22 @@ def _parse_object_common_part( # noqa: PLR0912, PLR0913, PLR0915
24772477
return self.data_type(reference=base_classes[0])
24782478
if required:
24792479
for field in fields:
2480-
if self.force_optional_for_required_fields or ( # pragma: no cover
2481-
self.apply_default_values_for_required_fields and field.has_default
2482-
):
2480+
if self.force_optional_for_required_fields: # pragma: no cover
24832481
continue # pragma: no cover
24842482
if (field.original_name or field.name) in required:
24852483
field.required = True
2484+
if self.apply_default_values_for_required_fields and field.has_default:
2485+
field.use_default_with_required = True
24862486
if obj.required:
24872487
field_name_to_field = {f.original_name or f.name: f for f in fields}
24882488
for required_ in obj.required:
24892489
if required_ in field_name_to_field:
24902490
field = field_name_to_field[required_]
2491-
if self.force_optional_for_required_fields or (
2492-
self.apply_default_values_for_required_fields and field.has_default
2493-
):
2491+
if self.force_optional_for_required_fields:
24942492
continue
24952493
field.required = True
2494+
if self.apply_default_values_for_required_fields and field.has_default:
2495+
field.use_default_with_required = True
24962496
else:
24972497
fields.append(
24982498
self.data_model_field_type(required=True, original_name=required_, data_type=DataType())
@@ -2828,24 +2828,23 @@ def parse_object_fields(
28282828
class_name=class_name,
28292829
)
28302830

2831-
if self.force_optional_for_required_fields or (
2832-
self.apply_default_values_for_required_fields and effective_has_default
2833-
):
2831+
if self.force_optional_for_required_fields:
28342832
required: bool = False
28352833
else:
28362834
required = original_field_name in requires
2837-
fields.append(
2838-
self.get_object_field(
2839-
field_name=field_name,
2840-
field=field,
2841-
required=required,
2842-
field_type=field_type,
2843-
alias=alias,
2844-
original_field_name=original_field_name,
2845-
effective_default=effective_default,
2846-
effective_has_default=effective_has_default,
2847-
)
2835+
new_field = self.get_object_field(
2836+
field_name=field_name,
2837+
field=field,
2838+
required=required,
2839+
field_type=field_type,
2840+
alias=alias,
2841+
original_field_name=original_field_name,
2842+
effective_default=effective_default,
2843+
effective_has_default=effective_has_default,
28482844
)
2845+
if required and self.apply_default_values_for_required_fields and effective_has_default:
2846+
new_field.use_default_with_required = True
2847+
fields.append(new_field)
28492848
return fields
28502849

28512850
def parse_object(

0 commit comments

Comments
 (0)