Fastapi: [BUG] FastAPI doesn't take in account object type when detecting response models

Created on 7 Feb 2020  路  4Comments  路  Source: tiangolo/fastapi

Describe the bug

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.

To Reproduce

  1. Let's imagine we have two models:
import pydantic

class Dog(pydantic.BaseModel):
    id: int
    name: str

class Owner(pydantic.BaseModel):
    id: int
    name: str
    address: str
    age: int
  1. And we have a route that can return both 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
  1. Open the browser and call the endpoint /.
  2. It returns a JSON with owner serialized as dog.

Expected behavior

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.

Environment

  • OS: Linux
  • FastAPI Version: 0.48.0

  • Python version: 3.7

bug

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 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.

All 4 comments

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.

Was this page helpful?
0 / 5 - 0 ratings