Validation of response model when using discriminated union
First Check
- [X] I added a very descriptive title to this issue.
- [X] I used the GitHub search to find a similar issue and didn't find it.
- [X] I searched the FastAPI documentation, with the integrated search.
- [X] I already searched in Google "How to X in FastAPI" and didn't find any information.
- [X] I already read and followed all the tutorial in the docs and didn't find an answer.
- [X] I already checked if it is not related to FastAPI but to Pydantic.
- [X] I already checked if it is not related to FastAPI but to Swagger UI.
- [X] I already checked if it is not related to FastAPI but to ReDoc.
Commit to Help
- [X] I commit to help with one of those options 👆
Example Code
from enum import Enum
from typing import Annotated, Union, Literal
from fastapi import FastAPI
from pydantic import BaseModel, Field, root_validator
class PetType(str, Enum):
CAT = "cat"
DOG = "dog"
class Cat(BaseModel):
kind: Literal[PetType.CAT]
has_paws: bool
color: str
@root_validator
def validate_cat(cls, values):
print("Validating Cat")
color = values.get('color')
if color.lower() != "yellow":
raise ValueError("Cat must be yellow")
return values
class Dog(BaseModel):
kind: Literal[PetType.DOG]
has_paws: bool
@root_validator
def validate_cat(cls, values):
print("Validating Dog")
return values
Pet = Annotated[Union[Cat, Dog], Field(discriminator='kind')]
class Animal(BaseModel):
pet: Pet
app = FastAPI()
@app.post("/", response_model=Animal)
def handle_pet(animal: Animal):
print(f"Pet has been discriminated: {type(animal.pet)}")
print(f"Animal can be re-created from dict: {Animal(**animal.dict())}")
return animal
Description
The POST / endpoint can be used normally with a pet Cat payload:
{
"pet": {
"kind": "cat",
"has_paws": true,
"color": "Yellow"
}
}
However, using a pet Dog payload:
{
"pet": {
"kind": "dog",
"has_paws": true
}
}
an error is raised, because the root_validator function of the Cat class is called.
This happens when the endpoint function returns. When validating the input payload, only the Dog root validator is called.
I know that this issue will likely also be linked to pydantic and discriminated unions; but why is FastAPI trying to validate the response against the Cat model? As noted, re-creating the Animal object from the object .dict() does not fail.
Operating System
macOS
Operating System Details
No response
FastAPI Version
0.79.0
Python Version
3.9.12
Additional Context
No response
# routing.py ln128
value, errors_ = field.validate(response_content, {}, loc=("response",))
else:
value, errors_ = await run_in_threadpool( # type: ignore[misc]
field.validate, response_content, {}, loc=("response",)
)
It fails at this point! I am not sure if the issue is with Pydantic or FastAPI. It seems to call the validate method on ModelField class, which is not handling the discrimination properly; and all this are happeing on Pydantic side.
Discussions related: https://github.com/samuelcolvin/pydantic/issues/3758
From above reference, we can get a better understanding.
Since response model are created with Optional it is no considering the discriminator.
To replicate, I tried this:
class Animal(BaseModel, ):
pet: Optional[Annotated[Union[Cat, Dog], Field(discriminator='kind')]]
Animal.validate({
"pet": {
"kind": "dog",
"has_paws": True
}
})