Summary
When an OpenAPI spec contains a oneOf + discriminator tagged union and generation runs with force_optional_for_required_fields=True, the generated pydantic v2 models are categorically invalid: the discriminator field on each union member becomes Literal["v"] | None = None, and pydantic refuses to build the discriminated union at class-creation time:
pydantic.errors.PydanticUserError: Model 'CardPayment' needs field 'kind' to be of type `Literal`
For further information visit https://errors.pydantic.dev/2.13/u/discriminator-needs-literal
With generate_dynamic_models this crashes generation itself; with string code generation the emitted source is syntactically fine but fails identically at import time.
Adding use_one_literal_as_default=True does not rescue it (see root cause).
Reproduction
from datamodel_code_generator import DataModelType, GenerateConfig, generate_dynamic_models
SPEC = {
"openapi": "3.1.0",
"info": {"title": "repro", "version": "0.1.0"},
"paths": {},
"components": {"schemas": {
"CardPayment": {
"type": "object",
"required": ["kind", "card_number"],
"properties": {
"kind": {"type": "string", "const": "card"},
"card_number": {"type": "integer"},
},
},
"CashPayment": {
"type": "object",
"required": ["kind", "received_amount"],
"properties": {
"kind": {"type": "string", "const": "cash"},
"received_amount": {"type": "integer"},
},
},
"Payment": {
"oneOf": [
{"$ref": "#/components/schemas/CardPayment"},
{"$ref": "#/components/schemas/CashPayment"},
],
"discriminator": {"propertyName": "kind", "mapping": {
"card": "#/components/schemas/CardPayment",
"cash": "#/components/schemas/CashPayment",
}},
},
}},
}
generate_dynamic_models(
SPEC,
config=GenerateConfig(
output_model_type=DataModelType.PydanticV2BaseModel,
force_optional_for_required_fields=True,
),
)
# PydanticUserError: Model 'CardPayment' needs field 'kind' to be of type `Literal`
Results across flag combinations (0.67.0, pydantic 2.13.4):
| flags |
outcome |
force_optional_for_required_fields=True |
❌ PydanticUserError: discriminator-needs-literal |
force_optional_for_required_fields=True, use_one_literal_as_default=True |
❌ same error |
use_one_literal_as_default=True only |
✅ generates, but required fields stay required (defeats the purpose of force-optional) |
Root cause
Ordering between the two transformations:
force_optional_for_required_fields is applied at field construction time — e.g. parser/jsonschema.py:5344: required = not (self.force_optional_for_required_fields or is_nullable). The discriminator field becomes optional/nullable there.
Parser.__set_one_literal_on_default (parser/base.py:2709) — the pass that would produce the pydantic-legal Literal["v"] = "v" shape — runs later over the constructed models and skips fields that are no longer required:
if not model_field.required or len(model_field.data_type.literals) != 1:
continue
So once force-optional has fired, no flag combination can produce a valid discriminated union: the discriminator field is emitted as Literal["v"] | None = None, which pydantic rejects, and the one flag that emits the accepted shape can never reach the field.
Suggested fix
Either (or both):
- Exempt discriminator properties from force-optional: when a field is the
propertyName of a discriminator object referencing its parent schema, emit Literal["v"] = "v" (optional-with-default — pydantic accepts a discriminator field with a default) instead of Literal["v"] | None = None.
- Extend
__set_one_literal_on_default to also cover force-optional'd single-literal fields (drop the required guard for fields whose type is a single literal and clear nullable), so force_optional_for_required_fields + use_one_literal_as_default composes into the valid shape.
Option 1 seems strictly correct even without the second flag: force_optional_for_required_fields producing models that pydantic itself refuses to construct is never useful output, and Literal["v"] = "v" is the optional form of a const discriminator.
Environment
- datamodel-code-generator: 0.67.0
- pydantic: 2.13.4
- Python: 3.13 (Linux x86_64)
Summary
When an OpenAPI spec contains a
oneOf+discriminatortagged union and generation runs withforce_optional_for_required_fields=True, the generated pydantic v2 models are categorically invalid: the discriminator field on each union member becomesLiteral["v"] | None = None, and pydantic refuses to build the discriminated union at class-creation time:With
generate_dynamic_modelsthis crashes generation itself; with string code generation the emitted source is syntactically fine but fails identically at import time.Adding
use_one_literal_as_default=Truedoes not rescue it (see root cause).Reproduction
Results across flag combinations (0.67.0, pydantic 2.13.4):
force_optional_for_required_fields=TruePydanticUserError: discriminator-needs-literalforce_optional_for_required_fields=True, use_one_literal_as_default=Trueuse_one_literal_as_default=TrueonlyRoot cause
Ordering between the two transformations:
force_optional_for_required_fieldsis applied at field construction time — e.g.parser/jsonschema.py:5344:required = not (self.force_optional_for_required_fields or is_nullable). The discriminator field becomes optional/nullable there.Parser.__set_one_literal_on_default(parser/base.py:2709) — the pass that would produce the pydantic-legalLiteral["v"] = "v"shape — runs later over the constructed models and skips fields that are no longerrequired:So once force-optional has fired, no flag combination can produce a valid discriminated union: the discriminator field is emitted as
Literal["v"] | None = None, which pydantic rejects, and the one flag that emits the accepted shape can never reach the field.Suggested fix
Either (or both):
propertyNameof adiscriminatorobject referencing its parent schema, emitLiteral["v"] = "v"(optional-with-default — pydantic accepts a discriminator field with a default) instead ofLiteral["v"] | None = None.__set_one_literal_on_defaultto also cover force-optional'd single-literal fields (drop therequiredguard for fields whose type is a single literal and clearnullable), soforce_optional_for_required_fields + use_one_literal_as_defaultcomposes into the valid shape.Option 1 seems strictly correct even without the second flag:
force_optional_for_required_fieldsproducing models that pydantic itself refuses to construct is never useful output, andLiteral["v"] = "v"is the optional form of a const discriminator.Environment