fastapi
fastapi copied to clipboard
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
}
})