← baseline_full_v2

fastapi_12942

failed NO_PATCH NO_PATCH UNSUBMITTED empty_patch(None) · None tool calls · 0 s · fastapi/fastapi

Task input

🐛 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 calls (0)

#ToolArgumentsResult
No trace captured.

Test output

show
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