Skip to content

--reuse-model injects Literal['reuse'] as discriminator value, breaking pydantic unions #3092

@bokelley

Description

@bokelley

Summary

When --reuse-model deduplicates inlined copies of the same discriminated union, the generated subclasses use the string literal 'reuse' as the discriminator const value. Two such subclasses then collide on that literal and pydantic rejects the union at import time with Value 'reuse' for discriminator '...' mapped to multiple choices.

No 'reuse' appears in the input schema — the literal is injected by the generator.

Version

  • datamodel-codegen 0.56.1
  • Python 3.14
  • Output mode: pydantic_v2.BaseModel

Minimal repro

minimal.json — two objects, each with a signal_id property inlining the same oneOf (discriminator: source, values: catalog/agent):

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Root",
  "type": "object",
  "properties": {
    "first_thing": {
      "type": "object",
      "properties": {
        "signal_id": {
          "title": "Signal ID",
          "discriminator": {"propertyName": "source"},
          "oneOf": [
            {"type": "object", "properties": {"source": {"const": "catalog"}, "domain": {"type": "string"}}, "required": ["source", "domain"]},
            {"type": "object", "properties": {"source": {"const": "agent"}, "url": {"type": "string"}}, "required": ["source", "url"]}
          ]
        }
      }
    },
    "second_thing": {
      "type": "object",
      "properties": {
        "signal_id": {
          "title": "Signal ID",
          "discriminator": {"propertyName": "source"},
          "oneOf": [
            {"type": "object", "properties": {"source": {"const": "catalog"}, "domain": {"type": "string"}}, "required": ["source", "domain"]},
            {"type": "object", "properties": {"source": {"const": "agent"}, "url": {"type": "string"}}, "required": ["source", "url"]}
          ]
        }
      }
    }
  }
}
datamodel-codegen \
  --input minimal.json \
  --input-file-type jsonschema \
  --output out.py \
  --output-model-type pydantic_v2.BaseModel \
  --reuse-model \
  --use-union-operator \
  --use-standard-collections

Actual output (broken)

class SignalId(BaseModel):
    source: Literal['catalog']
    domain: str

class SignalId1(BaseModel):
    source: Literal['agent']
    url: str

class FirstThing(BaseModel):
    signal_id: SignalId | SignalId1 | None = Field(None, discriminator='source', ...)

class SignalId2(SignalId):
    source: Literal['reuse']    # <-- injected, does not appear in input schema

class SignalId3(SignalId1):
    source: Literal['reuse']    # <-- same literal, collides with SignalId2

class SecondThing(BaseModel):
    signal_id: SignalId2 | SignalId3 | None = Field(None, discriminator='source', ...)

Importing this module raises:

TypeError: Value 'reuse' for discriminator 'source' mapped to multiple choices

Expected output

Either reuse SignalId | SignalId1 directly for second_thing.signal_id (true dedup), or produce fresh numbered classes that preserve the real discriminator values (catalog/agent), as happens without --reuse-model:

# same command, --reuse-model removed
datamodel-codegen --input minimal.json --input-file-type jsonschema --output out.py --output-model-type pydantic_v2.BaseModel --use-union-operator --use-standard-collections

Produces clean SignalId/SignalId1/SignalId2/SignalId3 with correct catalog/agent literals and no 'reuse' anywhere.

Context

Hit in the AdCP Python SDK where bundled JSON Schemas inline the same discriminated union across multiple audience-targeting variants. Full context: adcontextprotocol/adcp-client-python#184.

Workaround in our repo: post-generation step walks files, deletes class XN(Parent): source: Literal['reuse'] blocks, rewrites XN references back to Parent. Happy to contribute a fix upstream once you point to the right place in the reuse-model logic.

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