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 docs – Discriminated 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
“.