Skip to content

Commit e9cd243

Browse files
committed
Fix array RootModel default value handling in parser
1 parent e717208 commit e9cd243

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:
@@ -3054,7 +3056,7 @@ def parse_list_item(
30543056
for index, item in enumerate(target_items)
30553057
]
30563058

3057-
def parse_array_fields( # noqa: PLR0912
3059+
def parse_array_fields( # noqa: PLR0912, PLR0915
30583060
self,
30593061
name: str,
30603062
obj: JsonSchemaObject,
@@ -3069,12 +3071,19 @@ def parse_array_fields( # noqa: PLR0912
30693071
required: bool = False
30703072
nullable: Optional[bool] = None # noqa: UP045
30713073
else:
3072-
required = not (obj.has_default and self.apply_default_values_for_required_fields)
3074+
required = not obj.has_default
30733075
if self.strict_nullable:
30743076
nullable = obj.nullable if obj.has_default or required else True
30753077
else:
30763078
required = not obj.nullable and required
3077-
nullable = None
3079+
# Set nullable=False when has_default and schema doesn't explicitly allow null
3080+
# This prevents fall_back_to_nullable from adding | None
3081+
if obj.nullable:
3082+
nullable = True
3083+
elif obj.has_default:
3084+
nullable = False
3085+
else:
3086+
nullable = None
30783087
is_tuple = False
30793088
suppress_item_constraints = False
30803089
if isinstance(obj.items, JsonSchemaObject):
@@ -3259,10 +3268,35 @@ def parse_root_type( # noqa: PLR0912, PLR0914, PLR0915
32593268
data_type = self.data_type_manager.get_data_type(
32603269
Types.any,
32613270
)
3262-
required = self._should_field_be_required(
3263-
has_default=obj.has_default,
3264-
is_nullable=bool(obj.nullable),
3265-
)
3271+
# For RootModels (not TypeAlias):
3272+
# - required=False when nullable or has_default (preserves = None for nullable types)
3273+
# - nullable=False when has_default and not explicitly nullable (prevents fall_back_to_nullable)
3274+
# For TypeAlias (IS_ALIAS=True):
3275+
# - Defaults are meaningless for type aliases, so don't process has_default
3276+
is_type_alias = getattr(self.data_model_root_type, "IS_ALIAS", False)
3277+
if self.force_optional_for_required_fields:
3278+
required = False
3279+
nullable = None # Will be handled by fall_back_to_nullable
3280+
# Force optional makes all fields optional, so use None as default
3281+
has_default_override = True
3282+
default_value = obj.default if obj.has_default else None
3283+
elif obj.nullable:
3284+
required = False
3285+
nullable = True
3286+
# Nullable fields should have None as default when no explicit default is set
3287+
has_default_override = True
3288+
default_value = obj.default if obj.has_default else None
3289+
elif obj.has_default and not is_type_alias:
3290+
required = False
3291+
# Set nullable=False to prevent fall_back_to_nullable from adding | None
3292+
nullable = False
3293+
has_default_override = True
3294+
default_value = obj.default
3295+
else:
3296+
required = True
3297+
nullable = None
3298+
has_default_override = obj.has_default
3299+
default_value = obj.default if obj.has_default else UNDEFINED
32663300
name = self._apply_title_as_name(name, obj)
32673301
if not reference:
32683302
reference = self.model_resolver.add(path, name, loaded=True, class_name=True)
@@ -3276,20 +3310,18 @@ def parse_root_type( # noqa: PLR0912, PLR0914, PLR0915
32763310
fields=[
32773311
self.data_model_field_type(
32783312
data_type=data_type,
3279-
default=obj.default,
3313+
default=default_value,
32803314
required=required,
32813315
constraints=constraints,
3282-
nullable=obj.nullable
3283-
if self.strict_nullable and obj.nullable is not None
3284-
else (False if self.strict_nullable and obj.has_default else None),
3316+
nullable=nullable,
32853317
strip_default_none=self.strip_default_none,
32863318
extras=self.get_field_extras(obj),
32873319
use_annotated=self.use_annotated,
32883320
use_field_description=self.use_field_description,
32893321
use_field_description_example=self.use_field_description_example,
32903322
use_inline_field_description=self.use_inline_field_description,
32913323
original_name=None,
3292-
has_default=obj.has_default,
3324+
has_default=has_default_override,
32933325
)
32943326
],
32953327
custom_base_class=self._resolve_base_class(name, obj.custom_base_path),
@@ -3298,7 +3330,7 @@ def parse_root_type( # noqa: PLR0912, PLR0914, PLR0915
32983330
path=self.current_source_path,
32993331
nullable=obj.type_has_null,
33003332
treat_dot_as_module=self.treat_dot_as_module,
3301-
default=obj.default if obj.has_default else UNDEFINED,
3333+
default=default_value if has_default_override else UNDEFINED,
33023334
)
33033335
self.results.append(data_model_root_type)
33043336
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)