failed NO_PATCH NO_PATCH UNSUBMITTED empty_patch(None) · None tool calls · 0 s · fastapi/fastapi
🐛 Fix tagged discriminated union not recognized as body field
## Problem
When using a union type models with a tagged discriminator as an endpoint parameter, like the following:
```py
class Pie(BaseModel):
time_to_cook: int
num_ingredients: int
class ApplePie(Pie):
fruit: Literal["apple"] = "apple"
class PumpkinPie(Pie):
filling: Literal["pumpkin"] = "pumpkin"
def get_discriminator_value(v: Any) -> str:
if isinstance(v, dict):
return v.get("fruit", v.get("filling"))
return getattr(v, "fruit", getattr(v, "filling", None))
Dessert = Annotated[
Annotated[ApplePie, Tag("apple")] | Annotated[PumpkinPie, Tag("pumpkin")],
Discriminator(get_discriminator_value),
]
app = FastAPI()
@app.post("/dessert")
async def dinner(dessert: Dessert) -> dict[str, Any]:
return dessert.model_dump()
```
FastAPI fails to identify it correctly as **body** payload. It incorrectly considers it's a query parameter, which then causes a validation error. The workaround currently is to explicitly tell FastAPI it's a body parameter using `dessert: Annotated[Dessert, Body()]`
## Fix
The solution is to improve the function handling type annotations by handling the case where we can have `Annotated`
types inside an `Annotated` type.
---
Discussed in https://github.com/fastapi/fastapi/discussions/12941| # | Tool | Arguments | Result |
|---|---|---|---|
| No trace captured. | |||
F
=================================== FAILURES ===================================
________________________ test_discriminator_pydantic_v2 ________________________
@needs_pydanticv2
def test_discriminator_pydantic_v2() -> None:
from pydantic import Tag
app = FastAPI()
class FirstItem(BaseModel):
value: Literal["first"]
price: int
class OtherItem(BaseModel):
value: Literal["other"]
price: float
Item = Annotated[
Union[Annotated[FirstItem, Tag("first")], Annotated[OtherItem, Tag("other")]],
Field(discriminator="value"),
]
@app.post("/items/")
def save_union_body_discriminator(
item: Item, q: Annotated[str, Field(description="Query string")]
) -> Dict[str, Any]:
return {"item": item}
client = TestClient(app)
response = client.post("/items/?q=first", json={"value": "first", "price": 100})
> assert response.status_code == 200, response.text
E AssertionError: {"detail":[{"type":"missing","loc":["query","item"],"msg":"Field required","input":null}]}
E assert 422 == 200
E + where 422 = <Response [422 Unprocessable Entity]>.status_code
tests/test_union_body_discriminator.py:40: AssertionError
=============================== warnings summary ===============================
../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.48.0-py3-none-any/starlette/testclient.py:51
/Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.48.0-py3-none-any/starlette/testclient.py:51: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
_PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 1 warning in 0.54s