Top-level Discriminated Union with callable discriminator not supported? #12941
Replies: 3 comments 4 replies
-
|
I found a simple fix and took the liberty to open a PR for it: #12942 |
Beta Was this translation helpful? Give feedback.
-
|
I think it's too complicated structure for Query parameters. It's better to use Body in this case if it's possible. @app.post("/dessert")
async def dinner(
dessert: Annotated[Dessert, Body()],
) -> dict[str, Any]:
return dessert.model_dump()Works fine in current versions of FastAPI and Pydantic. Detailsfrom typing import Annotated, Any, Literal
import httpx
import pytest
from fastapi import Body, FastAPI
from pydantic import BaseModel, Discriminator, Tag
# Discriminated union with a callable discriminator
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 | None:
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),
]
# FastAPI app
app = FastAPI()
@app.post("/dessert")
async def dinner(
dessert: Annotated[Dessert, Body()],
) -> dict[str, Any]:
return dessert.model_dump()
# Tests
@pytest.mark.asyncio
async def test_dessert() -> None:
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app), base_url="http://app.local"
) as client:
response = await client.post(
"/dessert",
json={"time_to_cook": 42, "num_ingredients": 8, "fruit": "apple"},
)
assert response.status_code == 200
assert response.json() == {
"time_to_cook": 42,
"num_ingredients": 8,
"fruit": "apple",
}
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app), base_url="http://app.local"
) as client:
response = await client.post(
"/dessert",
json={"time_to_cook": 42, "num_ingredients": 8, "filling": "pumpkin"},
)
assert response.status_code == 200
assert response.json() == {
"time_to_cook": 42,
"num_ingredients": 8,
"filling": "pumpkin",
} |
Beta Was this translation helpful? Give feedback.
-
|
I've experienced the same behaviour without using a callable discriminator. Simply using the union of the different alternatives works as expected - with the parameter in the body. from typing import Any, Literal
from fastapi import FastAPI
from pydantic import BaseModel
# Discriminated union with implicit discriminator
class Pie(BaseModel):
time_to_cook: int
num_ingredients: int
class ApplePie(Pie):
fruit: Literal["apple"] = "apple"
class PumpkinPie(Pie):
filling: Literal["pumpkin"] = "pumpkin"
# Select one of the below definitions
type Dessert = ApplePie | PumpkinPie # Results in `dessert` being a query param
Dessert = ApplePie | PumpkinPie # Results in `dessert` being a body param (as desired)
# FastAPI app
app = FastAPI()
@app.post("/dessert")
async def dinner(dessert: Dessert) -> dict[str, Any]:
return dessert.model_dump()@frankie567, can your fix also handle this case? |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
First Check
Commit to Help
Example Code
Description
I'm implementing an endpoint with a payload schema defined as a Discriminated Union with a Callable Discriminator.
In this case, FastAPI seems unable to correctly validate it, and return a 422 error which is a bit off-topic:
{ "detail": [ { "input": null, "loc": [ "query", "dessert" ], "msg": "Field required", "type": "missing" } ] }It seems FastAPI incorrectly identifies the field as a query field instead of a body field.
Operating System
macOS
Operating System Details
Sequoia 15.1 - Apple Silicon
FastAPI Version
0.115.5
Pydantic Version
2.9.2
Python Version
3.12.7
Additional Context
Solution 1 (not working)
Adding
Body(...)as a default argument to the parameter makes it seem to work:But actually, the discriminator is not called at all in this case.
Full code
Solution 2 (working, but type checker unhappy)
Adding
Bodyand repeating thediscriminatorargument on it makes it work, butmypyis unhappy sinceBodyexpectsstr | Nonefor this parameter`:Full code
Investigation
My guess is that FastAPI is troubled by the "sub-annotated" model with the
Tagconstruct likeAnnotated[ApplePie, Tag("apple")], explaining why it sees it as a query parameter by default. Probably something around here:fastapi/fastapi/dependencies/utils.py
Lines 457 to 460 in 1cfea40
fastapi/fastapi/_compat.py
Lines 562 to 580 in 1cfea40
Beta Was this translation helpful? Give feedback.
All reactions