Path, query and body parameters in depth
Control every input your API accepts: typed path segments, enums, query aliases and validation, body embedding, and the modern Annotated style.
Path parameters
A parameter in the route string is a path parameter. FastAPI converts it using the type annotation, and a value that cannot be parsed returns 422 before your function runs.
from enum import Enum
from fastapi import FastAPI, Path
from typing import Annotated
app = FastAPI()
class Tier(str, Enum):
free = "free"
pro = "pro"
enterprise = "enterprise"
@app.get("/users/{user_id}")
def get_user(user_id: Annotated[int, Path(ge=1, description="Database id")]):
return {"user_id": user_id}
@app.get("/plans/{tier}")
def plan(tier: Tier):
return {"tier": tier, "price": {"free": 0, "pro": 19, "enterprise": 99}[tier.value]}- An
Enumsubclass ofstrrestricts the route to a fixed set of values and documents them in OpenAPI. Path(ge=1)adds a numeric constraint; the failure is a validation error, not a server crash.- Decorate fixed routes before parameterised ones, or
/users/{user_id}will swallow/users/me.
Query parameters
Any parameter that is not in the path and is a scalar type becomes a query parameter. Giving it a default makes it optional; typing it as Optional or with a default of None makes it nullable.
from fastapi import Query
@app.get("/search")
def search(
q: Annotated[str, Query(min_length=2, max_length=80)],
page: Annotated[int, Query(ge=1)] = 1,
size: Annotated[int, Query(ge=1, le=100)] = 20,
sort: Annotated[str, Query(alias="order-by")] = "created_at",
tags: Annotated[list[str] | None, Query()] = None,
):
return {"q": q, "page": page, "size": size, "sort": sort, "tags": tags or []}# repeated keys fill a list
curl "http://127.0.0.1:8000/search?q=hello&size=5&order-by=name&tags=a&tags=b"| Declaration | Resulting parameter |
|---|---|
q: str | Required query parameter |
q: str = "x" | Optional with a default |
q: str | None = None | Optional and nullable |
q: Annotated[str, Query(alias="term")] | Reads from ?term=, documents the real name |
q: list[str] = [] | Repeated key, collected into a list |
Body models and the Annotated style
from pydantic import BaseModel, Field
from fastapi import Body
class Item(BaseModel):
name: str
price: float = Field(gt=0)
class Owner(BaseModel):
email: str
notify: bool = False
@app.put("/items/{item_id}")
def update(
item_id: int,
item: Item,
owner: Owner, # two models = two top-level keys
reason: Annotated[str, Body(embed=True)], # single scalar needs embedding
):
return {"item_id": item_id, "item": item, "owner": owner, "reason": reason}💡
Two Pydantic parameters expect a body of the form
{"item": {...}, "owner": {...}, "reason": "..."}. A single model would be the whole body; a single scalar without embed=True would be rejected because FastAPI refuses to guess a key name for it.FAQ
How do I accept arbitrary extra fields in a body?
Declare the model with
model_config = ConfigDict(extra="allow") to keep them, or extra="forbid" to reject them with a validation error. The default, ignore, silently drops unknown keys, which hides client bugs.Why does my optional query parameter 422 when I omit it?
The annotation is
Optional but the default is missing, so the field is still required. Give it = None as well.Related
Your first FastAPI app Pydantic request and response models
Last refreshed 2026-09-18.