If you have a response_model set to a Union and objects inside the union are slightly similar, FastAPI will take the first one to serialize even if we return the explicit object.
import pydantic
class Dog(pydantic.BaseModel):
id: int
name: str
class Owner(pydantic.BaseModel):
id: int
name: str
address: str
age: int
Dog or Owner:from models import Dog, Owner
from fastapi import FastAPI
from typing import List, Union
app = FastAPI()
@app.get("/", response_model=List[Union[Dog, Owner]])
def read_obj():
# Do something and get owner
owners = [Owner(id=1, name="Bob", address="Av. X", age=52), Owner(id=2, name="Joe", address="Av. Y", age=5)]
return owners
/.FastAPI should try to cast response models based on the type of the object first and then try to serialize it based on the attributes.
FastAPI Version: 0.48.0
Python version: 3.7
This is not a FastAPI-related issue, but an upstream pydantic feature. Please see: https://pydantic-docs.helpmanual.io/usage/types/#unions
import pydantic
from typing import List, Union
from pydantic.fields import FieldInfo, ModelField
class Dog(pydantic.BaseModel):
id: int
name: str
class Owner(pydantic.BaseModel):
id: int
name: str
address: str
age: int
field = ModelField(
name='response',
type_=List[Union[Dog, Owner]],
class_validators={},
default=None,
required=False,
model_config=pydantic.BaseConfig,
field_info=FieldInfo(None),
)
print(field.validate([Owner(id=1, name="Bob", address="Av. X", age=52), Owner(id=2, name="Joe", address="Av. Y", age=5)], {}, loc=()))
Yep. As @phy25 says :point_up:
Gotcha, thank you guys for the clarification
@guiscaranse @phy25 @tiangolo To be fair, if FastAPI didn't reparse the result before returning it, I think it would be serialized "properly". I think the current behavior is confusing (and borderline buggy) since actual instances of Owner are getting reparsed into instances of not-even-the-corresponding-secure-cloned-type.
Also, @guiscaranse if you just change List[Union[Dog, Owner]] to List[Union[Owner, Dog]], it should fix your issues -- that way, pydantic will always try to parse as an Owner first, then fall back to Dog if parsing as Owner fails.
The more careful way to handle this, and more generally the most OpenAPI-friendly way to work with Union types is to add a "discriminator" field when returning a Union:
import pydantic
from typing_extensions import Literal # or just `from typing import Literal` in python 3.8
class Dog(pydantic.BaseModel):
type_: Literal["dog"] = pydantic.Field("dog", alias="type")
id: int
name: str
class Owner(pydantic.BaseModel):
type_: Literal["owner"] = pydantic.Field("owner", alias="type")
id: int
name: str
address: str
age: int
With this approach, the model will only parse properly if you specify the exact correct value of the type_ attribute (which you won't have to specify if instantiating from python code since it can use the default value). As written above, it would be possible to parse input JSON data that hadn't specified a "type", which may be undesirable, but it would likely solve the problem you are currently facing. (And if you dropped the default value for the discriminator it would prevent that problem, though would require you to specify the value during instantiation.)
Keep an eye out for a DiscriminatedUnion type that may be added in a future version of pydantic (it has been discussed, but I don't think there is a formal plan for it just yet), which would handle this case properly.
Most helpful comment
@guiscaranse @phy25 @tiangolo To be fair, if FastAPI didn't reparse the result before returning it, I think it would be serialized "properly". I think the current behavior is confusing (and borderline buggy) since actual instances of
Ownerare getting reparsed into instances of not-even-the-corresponding-secure-cloned-type.Also, @guiscaranse if you just change
List[Union[Dog, Owner]]toList[Union[Owner, Dog]], it should fix your issues -- that way, pydantic will always try to parse as anOwnerfirst, then fall back toDogif parsing asOwnerfails.The more careful way to handle this, and more generally the most OpenAPI-friendly way to work with
Uniontypes is to add a "discriminator" field when returning aUnion:With this approach, the model will only parse properly if you specify the exact correct value of the
type_attribute (which you won't have to specify if instantiating from python code since it can use the default value). As written above, it would be possible to parse input JSON data that hadn't specified a"type", which may be undesirable, but it would likely solve the problem you are currently facing. (And if you dropped the default value for the discriminator it would prevent that problem, though would require you to specify the value during instantiation.)Keep an eye out for a
DiscriminatedUniontype that may be added in a future version of pydantic (it has been discussed, but I don't think there is a formal plan for it just yet), which would handle this case properly.