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 Enum subclass of str restricts 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"
DeclarationResulting parameter
q: strRequired query parameter
q: str = "x"Optional with a default
q: str | None = NoneOptional 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.

Your first FastAPI app Pydantic request and response models

Last refreshed 2026-09-18.