Skip to content

Commit 22b9b8f

Browse files
committed
Fix array RootModel default value handling in parser
1 parent 88c7fe4 commit 22b9b8f

6 files changed

Lines changed: 52 additions & 20 deletions

File tree

docs/cli-reference/field-customization.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3912,7 +3912,7 @@ This is useful when schemas have descriptive titles that should be preserved.
39123912
class ProcessingStatusUnionTitle(BaseModel):
39133913
__root__: (
39143914
ProcessingStatusDetail | ExtendedProcessingTask | ProcessingStatusTitle
3915-
) = Field(..., title='Processing Status Union Title')
3915+
) = Field('COMPLETED', title='Processing Status Union Title')
39163916

39173917

39183918
class ProcessingTaskTitle(BaseModel):

src/datamodel_code_generator/model/pydantic/base_model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def __str__(self) -> str: # noqa: PLR0912
220220
elif isinstance(discriminator, dict): # pragma: no cover
221221
data["discriminator"] = discriminator["propertyName"]
222222

223-
if self.required and not self.has_default:
223+
if self.required:
224224
default_factory = None
225225
elif self.default is not UNDEFINED and self.default is not None and "default_factory" not in data:
226226
default_factory = self._get_default_as_pydantic_model()
@@ -249,7 +249,7 @@ def __str__(self) -> str: # noqa: PLR0912
249249

250250
if self.use_annotated:
251251
field_arguments = self._process_annotated_field_arguments(field_arguments)
252-
elif self.required and not default_factory:
252+
elif self.required:
253253
field_arguments = ["...", *field_arguments]
254254
elif not default_factory:
255255
default_repr = repr_set_sorted(self.default) if isinstance(self.default, set) else repr(self.default)

src/datamodel_code_generator/model/template/pydantic/BaseModel_root.jinja2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class {{ class_name }}({{ base_class }}):{% if comment is defined %} # {{ comme
2424
{%- else %}
2525
__root__: {{ field.type_hint }}
2626
{%- endif %}
27-
{%- if not field.has_default_factory_in_field and not ((field.required and not field.has_default) or (field.represented_default == 'None' and field.strip_default_none))
27+
{%- if not field.has_default_factory_in_field and not (field.required or (field.represented_default == 'None' and field.strip_default_none))
2828
%} = {{ field.represented_default }}
2929
{%- endif -%}
3030
{%- endif %}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class {{ class_name }}({{ base_class }}{%- if fields -%}[{{get_type_hint(fields,
4242
{%- else %}
4343
root: {{ field.type_hint }}
4444
{%- endif %}
45-
{%- if not field.has_default_factory_in_field and not ((field.required and not field.has_default) or (field.represented_default == 'None' and field.strip_default_none))
45+
{%- if not field.has_default_factory_in_field and not (field.required or (field.represented_default == 'None' and field.strip_default_none))
4646
%} = {{ field.represented_default }}
4747
{%- endif -%}
4848
{%- endif %}

src/datamodel_code_generator/parser/jsonschema.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -897,13 +897,15 @@ def _create_synthetic_enum_obj(
897897
final_enum = [*enum_values, None] if nullable else enum_values
898898
final_varnames = varnames if len(varnames) == len(enum_values) else []
899899

900+
# Only include default if original has one; otherwise don't set it at all
901+
# to avoid incorrectly marking the synthetic object as having a default
900902
return self.SCHEMA_OBJECT_TYPE(
901903
type=enum_type,
902904
enum=final_enum,
903905
title=original.title,
904906
description=original.description,
905907
x_enum_varnames=final_varnames,
906-
default=original.default if original.has_default else None,
908+
**({"default": original.default} if original.has_default else {}),
907909
)
908910

909911
def is_constraints_field(self, obj: JsonSchemaObject) -> bool:
@@ -3051,7 +3053,7 @@ def parse_list_item(
30513053
for index, item in enumerate(target_items)
30523054
]
30533055

3054-
def parse_array_fields( # noqa: PLR0912
3056+
def parse_array_fields( # noqa: PLR0912, PLR0915
30553057
self,
30563058
name: str,
30573059
obj: JsonSchemaObject,
@@ -3066,12 +3068,19 @@ def parse_array_fields( # noqa: PLR0912
30663068
required: bool = False
30673069
nullable: Optional[bool] = None # noqa: UP045
30683070
else:
3069-
required = not (obj.has_default and self.apply_default_values_for_required_fields)
3071+
required = not obj.has_default
30703072
if self.strict_nullable:
30713073
nullable = obj.nullable if obj.has_default or required else True
30723074
else:
30733075
required = not obj.nullable and required
3074-
nullable = None
3076+
# Set nullable=False when has_default and schema doesn't explicitly allow null
3077+
# This prevents fall_back_to_nullable from adding | None
3078+
if obj.nullable:
3079+
nullable = True
3080+
elif obj.has_default:
3081+
nullable = False
3082+
else:
3083+
nullable = None
30753084
is_tuple = False
30763085
suppress_item_constraints = False
30773086
if isinstance(obj.items, JsonSchemaObject):
@@ -3256,10 +3265,35 @@ def parse_root_type( # noqa: PLR0912, PLR0914, PLR0915
32563265
data_type = self.data_type_manager.get_data_type(
32573266
Types.any,
32583267
)
3259-
required = self._should_field_be_required(
3260-
has_default=obj.has_default,
3261-
is_nullable=bool(obj.nullable),
3262-
)
3268+
# For RootModels (not TypeAlias):
3269+
# - required=False when nullable or has_default (preserves = None for nullable types)
3270+
# - nullable=False when has_default and not explicitly nullable (prevents fall_back_to_nullable)
3271+
# For TypeAlias (IS_ALIAS=True):
3272+
# - Defaults are meaningless for type aliases, so don't process has_default
3273+
is_type_alias = getattr(self.data_model_root_type, "IS_ALIAS", False)
3274+
if self.force_optional_for_required_fields:
3275+
required = False
3276+
nullable = None # Will be handled by fall_back_to_nullable
3277+
# Force optional makes all fields optional, so use None as default
3278+
has_default_override = True
3279+
default_value = obj.default if obj.has_default else None
3280+
elif obj.nullable:
3281+
required = False
3282+
nullable = True
3283+
# Nullable fields should have None as default when no explicit default is set
3284+
has_default_override = True
3285+
default_value = obj.default if obj.has_default else None
3286+
elif obj.has_default and not is_type_alias:
3287+
required = False
3288+
# Set nullable=False to prevent fall_back_to_nullable from adding | None
3289+
nullable = False
3290+
has_default_override = True
3291+
default_value = obj.default
3292+
else:
3293+
required = True
3294+
nullable = None
3295+
has_default_override = obj.has_default
3296+
default_value = obj.default if obj.has_default else UNDEFINED
32633297
name = self._apply_title_as_name(name, obj)
32643298
if not reference:
32653299
reference = self.model_resolver.add(path, name, loaded=True, class_name=True)
@@ -3273,20 +3307,18 @@ def parse_root_type( # noqa: PLR0912, PLR0914, PLR0915
32733307
fields=[
32743308
self.data_model_field_type(
32753309
data_type=data_type,
3276-
default=obj.default,
3310+
default=default_value,
32773311
required=required,
32783312
constraints=constraints,
3279-
nullable=obj.nullable
3280-
if self.strict_nullable and obj.nullable is not None
3281-
else (False if self.strict_nullable and obj.has_default else None),
3313+
nullable=nullable,
32823314
strip_default_none=self.strip_default_none,
32833315
extras=self.get_field_extras(obj),
32843316
use_annotated=self.use_annotated,
32853317
use_field_description=self.use_field_description,
32863318
use_field_description_example=self.use_field_description_example,
32873319
use_inline_field_description=self.use_inline_field_description,
32883320
original_name=None,
3289-
has_default=obj.has_default,
3321+
has_default=has_default_override,
32903322
)
32913323
],
32923324
custom_base_class=self._resolve_base_class(name, obj.custom_base_path),
@@ -3295,7 +3327,7 @@ def parse_root_type( # noqa: PLR0912, PLR0914, PLR0915
32953327
path=self.current_source_path,
32963328
nullable=obj.type_has_null,
32973329
treat_dot_as_module=self.treat_dot_as_module,
3298-
default=obj.default if obj.has_default else UNDEFINED,
3330+
default=default_value if has_default_override else UNDEFINED,
32993331
)
33003332
self.results.append(data_model_root_type)
33013333
return self.data_type(reference=reference)

tests/data/expected/main/jsonschema/titles_use_title_as_name.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class ExtendedProcessingTasksTitle(BaseModel):
4747
class ProcessingStatusUnionTitle(BaseModel):
4848
__root__: (
4949
ProcessingStatusDetail | ExtendedProcessingTask | ProcessingStatusTitle
50-
) = Field(..., title='Processing Status Union Title')
50+
) = Field('COMPLETED', title='Processing Status Union Title')
5151

5252

5353
class ProcessingTaskTitle(BaseModel):

0 commit comments

Comments
 (0)