Skip to content

force_optional_for_required_fields + OpenAPI discriminator generates models pydantic rejects (discriminator-needs-literal); use_one_literal_as_default cannot compose to fix it #3606

Description

@RationallyPrime

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:

  1. 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.
  2. 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):

  1. 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.
  2. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions