FastAPI 0.95.0 upgrade – problem with pydantic unions

FastAPI 0.95.0 came with a great enhancement: support for dependencies and parameters using Annotated. But more about this I’ll wrote in a separate blog post. While updating my project dependency and changing FastAPI version from 0.94.1 to 0.95.0 I encountered a problem connected with a regression (?) that was introduced with above enhancement and adding support for PEP-593 in general. There is a problem with pydantic unions used in body params.

Problem context

In the code there is a place where I use pydantic union to indicate body parameter type. A quick example of how it works:

from typing import Literal, Union
from typing_extensions import Annotated

from fastapi import FastAPI
from pydantic import BaseModel, Field


class FirstType(BaseModel):
    type_name: Literal["first"] = "first"

class SecondType(BaseModel):
    type_name: Literal["second"] = "second"

ItemType = Annotated[
    Union[FirstType, SecondType],
    Field(discriminator="type_name"),
]

app = FastAPI()

@app.post('/')
async def create(item: ItemType) -> None:
    pass

In above example Field with discriminator set to type_name will indicate if item is of type FirstType or SecondType automatically by required and validated type_name value (will only accept “first” or “second” values). It was done based on pydantic docsDiscriminated Unions.

And it was working great… till introducing the upgrade. After the upgrade the problem started failing a bunch of unit tests with an assert:

AssertionError: Param: item can only be a request body, using Body()

Working solution

While a few moments of research I found out that the problem was already reported. And the suggested solution I tried and tested is to replace pydantic.Field with fastapi.Body.

So the above example with have a change:

from typing import Literal, Union
from typing_extensions import Annotated

- from fastapi import FastAPI
- from pydantic import BaseModel, Field
+ from fastapi import Body, FastAPI
+ from pydantic import BaseModel


class FirstType(BaseModel):
    type_name: Literal["first"] = "first"

class SecondType(BaseModel):
    type_name: Literal["second"] = "second"

ItemType = Annotated[
    Union[FirstType, SecondType],
-   Field(discriminator="type_name"),
+   Body(discriminator="type_name"),
]

app = FastAPI()

@app.post('/')
async def create(item: ItemType) -> None:
    pass


That’s all. As @phillipuniverse mentioned under the proposed solution, it probably shouldn’t even work before and usage of Body class came with an enhancement to automatically generated OpenAPI. “Before, the union had generated an anyOf, but now it generates the correct discriminated union with oneOf“.

Leave a comment

Your email address will not be published. Required fields are marked *