Declare a function parameter typed as a Pydantic BaseModel subclass. FastAPI
reads the request body as JSON, parses it, and validates it against the model.
from pydantic import BaseModel
from fastapi import FastAPI
class Item(BaseModel):
name: str
price: float
in_stock: bool = True
app = FastAPI()
@app.post("/items")
async def create_item(item: Item):
return item
# POST /items body: {"name": "Widget", "price": 9.99}
# → Item(name='Widget', price=9.99, in_stock=True)
FastAPI returns 422 if the body is missing, malformed JSON, or fails validation.
Rule of thumb: one Pydantic model = one request body; the model name appears in the OpenAPI schema as the expected input shape.
Annotate the body parameter with T | None = None:
class Item(BaseModel):
name: str
price: float
@app.patch("/items/{id}")
async def update_item(id: int, item: Item | None = None):
if item is None:
return {"updated": False}
return {"updated": True, "item": item}
A None default makes the body optional; FastAPI won't error if the client
sends an empty body. For partial updates prefer a model where all fields are
Optional rather than making the whole body optional.
Rule of thumb: for PATCH endpoints, make individual fields optional inside the model rather than making the entire body optional.
Nest a BaseModel inside another BaseModel. FastAPI validates the full
nested structure and generates the OpenAPI schema with $ref references.
class Address(BaseModel):
street: str
city: str
postcode: str
class User(BaseModel):
name: str
email: str
address: Address # nested model
@app.post("/users")
async def create_user(user: User):
return user
# body: {"name": "Alice", "email": "a@b.com",
# "address": {"street": "1 Main St", "city": "London", "postcode": "SW1A"}}
Arbitrarily deep nesting is supported; Pydantic validates all levels.
Rule of thumb: prefer deep models over flat ones with address_street,
address_city prefixes — nested models are cleaner and reusable across endpoints.
When you declare two Pydantic model parameters, FastAPI expects the body to be a JSON object keyed by parameter name:
class Item(BaseModel):
name: str
price: float
class Supplier(BaseModel):
name: str
contact: str
@app.post("/items")
async def create_item(item: Item, supplier: Supplier):
return {"item": item, "supplier": supplier}
# body: {"item": {"name": "Widget", "price": 9.99},
# "supplier": {"name": "Acme", "contact": "info@acme.com"}}
Rule of thumb: multiple body models wrap themselves under their parameter names automatically; use this to bundle related but distinct payloads in one request.
Use Body(embed=True) on the scalar value. This forces FastAPI to wrap it under
its parameter name in the expected JSON object.
from fastapi import Body
from typing import Annotated
class Item(BaseModel):
name: str
@app.put("/items/{id}")
async def update_item(
id: int,
item: Item,
importance: Annotated[int, Body(ge=1, le=5, embed=True)],
):
return {"item": item, "importance": importance}
# body: {"item": {"name": "Widget"}, "importance": 3}
Without embed=True, FastAPI would expect importance at the top level of the
JSON, which conflicts with item's keys.
Rule of thumb: any singular Body() value alongside a Pydantic model needs
embed=True so FastAPI knows to namespace it.
By default (Pydantic v2), extra fields are ignored — they're stripped silently and don't reach the handler.
class Item(BaseModel):
name: str
price: float
# body: {"name": "Widget", "price": 9.99, "secret": "ignored"}
# → Item(name='Widget', price=9.99) — secret dropped
You can change this behaviour via model_config:
from pydantic import BaseModel, ConfigDict
class StrictItem(BaseModel):
model_config = ConfigDict(extra="forbid") # 422 if extra fields sent
name: str
price: float
Options: "ignore" (default), "allow" (keep in model.model_extra),
"forbid" (raise validation error).
Rule of thumb: use extra="forbid" for request bodies to catch client typos
early; use extra="ignore" for internal models where schema drift is acceptable.
Type-annotate the body parameter as list[MyModel]:
@app.post("/items/bulk")
async def bulk_create(items: list[Item]):
return {"count": len(items)}
# body: [{"name": "A", "price": 1.0}, {"name": "B", "price": 2.0}]
FastAPI validates each element against Item. For a top-level list, there is
no "outer key" — the raw JSON array is the entire body.
Rule of thumb: use bulk endpoints for batch operations; add a reasonable size
limit (len(items) <= 100) inside the handler to prevent abuse.
Use Form() annotation and install python-multipart:
from fastapi import Form
from typing import Annotated
@app.post("/login")
async def login(
username: Annotated[str, Form()],
password: Annotated[str, Form()],
):
return {"user": username}
# Content-Type: application/x-www-form-urlencoded
# body: username=alice&password=secret
You cannot mix Form() and a Pydantic JSON body in the same handler — HTTP
only allows one Content-Type per request.
Rule of thumb: use Form() for OAuth2 password flows and HTML <form> submissions;
use JSON body everywhere else.
Use UploadFile for the file and Form() for accompanying fields. Both are
multipart/form-data.
from fastapi import UploadFile, Form
from typing import Annotated
@app.post("/upload")
async def upload(
file: UploadFile,
description: Annotated[str, Form()],
):
content = await file.read()
return {
"filename": file.filename,
"size": len(content),
"description": description,
}
UploadFile wraps a SpooledTemporaryFile; use await file.read() for small
files, iterate in chunks for large ones.
Rule of thumb: always await file.seek(0) before re-reading a file that was
already partially read elsewhere in the handler.
HTTP 422 Unprocessable Entity with a JSON body listing each failing field:
{
"detail": [
{
"type": "missing",
"loc": ["body", "price"],
"msg": "Field required",
"input": {"name": "Widget"},
"url": "https://errors.pydantic.dev/2.0/v/missing"
},
{
"type": "float_parsing",
"loc": ["body", "price"],
"msg": "Input should be a valid number",
"input": "not-a-number"
}
]
}
loc is a breadcrumb path: ["body", "field_name"] for top-level fields,
["body", "nested", "field"] for nested models.
Rule of thumb: parse detail[*].loc in client code to map validation errors
back to specific form fields for UX display.
FastAPI expects Content-Type: application/json for Pydantic body parameters.
If the client sends a different or missing Content-Type, FastAPI attempts to
parse the body as JSON anyway — but tools like Swagger UI always send the header.
POST /items HTTP/1.1
Content-Type: application/json
{"name": "Widget", "price": 9.99}
For form data: application/x-www-form-urlencoded or multipart/form-data.
For raw file streams: application/octet-stream with Request body directly.
Rule of thumb: always set Content-Type: application/json explicitly in REST
clients — don't rely on default behaviour.
Inject the Request object and call await request.body():
from fastapi import Request
@app.post("/webhook")
async def webhook(request: Request):
raw = await request.body() # bytes
payload = json.loads(raw)
signature = request.headers.get("X-Signature")
verify_signature(raw, signature) # HMAC check on raw bytes
return {"ok": True}
You cannot mix await request.body() with a Pydantic body parameter in the same
handler — FastAPI reads the body stream once; body() consumes it before Pydantic can.
Rule of thumb: use raw body access for webhooks that need HMAC verification on the exact bytes; use Pydantic models for everything else.
FastAPI has no built-in body size limit. Enforce it in middleware:
from fastapi import Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
MAX_BODY = 1 * 1024 * 1024 # 1 MB
class LimitBodyMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
if request.headers.get("content-length"):
if int(request.headers["content-length"]) > MAX_BODY:
return JSONResponse({"detail": "Body too large"}, status_code=413)
return await call_next(request)
app.add_middleware(LimitBodyMiddleware)
For production, configure the limit at the reverse proxy (Nginx client_max_body_size
or Uvicorn --limit-concurrency) rather than in application code.
Rule of thumb: enforce body size at the reverse proxy level for efficiency; add middleware as a defence-in-depth layer inside the app.
More Routing & Parameters interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.