Skip to content

Discriminated Unions Break When Wrapped in Annotated[Union, Body(...)] in FastAPI 0.124.1+ #14508

@tiangolo

Description

@tiangolo

Discussed in #14495

Originally posted by atlasale December 11, 2025

First Check

  • I added a very descriptive title here.
  • I used the GitHub search to find a similar question and didn't find it.
  • I searched the FastAPI documentation, with the integrated search.
  • I already searched in Google "How to X in FastAPI" and didn't find any information.
  • I already read and followed all the tutorial in the docs and didn't find an answer.
  • I already checked if it is not related to FastAPI but to Pydantic.
  • I already checked if it is not related to FastAPI but to Swagger UI.
  • I already checked if it is not related to FastAPI but to ReDoc.

Commit to Help

  • I commit to help with one of those options 👆

Example Code

from typing import Annotated, Union
from pydantic import BaseModel, Tag, Discriminator
from fastapi import FastAPI, Body

class Cat(BaseModel):
    pet_type: str = "cat"
    meows: int

class Dog(BaseModel):
    pet_type: str = "dog"
    barks: float

def get_pet_type(v):
    if isinstance(v, dict):
        return v.get("pet_type", "")
    return getattr(v, "pet_type", "")

# Define discriminated union
Pet = Annotated[
    Union[
        Annotated[Cat, Tag("cat")],
        Annotated[Dog, Tag("dog")]
    ],
    Discriminator(get_pet_type)
]

app = FastAPI()

# ✅ THIS WORKS in 0.124.1+
@app.post("/pet/works")
async def create_pet_works(pet: Pet = Body(...)):
    return pet

# ❌ THIS BREAKS in 0.124.1+ (worked in 0.124.0)
@app.post("/pet/broken")
async def create_pet_broken(pet: Annotated[Pet, Body(...)]):
    return pet

Description

I'm experiencing a breaking change when upgrading from FastAPI 0.124.0 to
0.124.1/0.124.2.

What works:

 - FastAPI 0.124.0 - Both endpoint patterns work correctly
 - FastAPI 0.124.1+ - Only pet: Pet = Body(...) works

What breaks:

 - FastAPI 0.124.1+ - The pattern pet: Annotated[Pet, Body(...)] fails

with:

 pydantic.errors.PydanticUserError: `Tag` not provided for choice {
   'type': 'tagged-union',
   'choices': {
     'cat': {'type': 'definition-ref', 'metadata':

{'pydantic_internal_union_tag_key': 'cat'}},
'dog': {'type': 'definition-ref', 'metadata':
{'pydantic_internal_union_tag_key': 'dog'}}
},
'discriminator': <function get_pet_type at 0x...>
} used with Discriminator

 For further information visit

https://errors.pydantic.dev/2.12/u/callable-discriminator-no-tag

Root Cause: This appears to be caused by commit 42b250d (https://github.com
/fastapi/fastapi/commit/42b250d14dd42d3c0c24dd085fa53878172a985f) which
fixed arbitrary_types_allowed=True but introduced a regression in how
discriminated unions are handled.

The change in fastapi/_compat/v2.py now includes *self.field_info.metadata
when creating TypeAdapters, which causes discriminator metadata to be
duplicated/nested when the union is wrapped in Annotated[..., Body(...)].

Workaround: Use pet: Pet = Body(...) instead of pet: Annotated[Pet,
Body(...)]

Operating System

Linux, macOS

Operating System Details

No response

FastAPI Version

0.124.2

Pydantic Version

2.12.5

Python Version

3.12

Additional Context

  • The error shows tags ARE present ('pydantic_internal_union_tag_key':
    'cat'), so Pydantic sees them but validation fails

    • This only affects callable discriminators with Tag(), not field-based
      discriminators with Literal
    • Both patterns (= Body(...) and Annotated[..., Body(...)]) should be
      functionally equivalent
    • This is a breaking change in a patch release

    Is this a known issue? Should I file a bug report?

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions