A request body is the JSON (or form, or file) that a client sends along with a POST/PUT/PATCH. In FastAPI we describe the expected shape with a Pydantic model — that gives us validation, serialization, and OpenAPI schema all in one go.
The basic model
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
in_stock: bool = True
@app.post("/items/")
async def create_item(item: Item):
return {"created": item, "total_with_tax": item.price * 1.18}
Three things happen automatically:
- FastAPI reads the request body as JSON.
- Validates it against
Item— wrong types or missing required fields → 422. - Hands us a real
Iteminstance with attribute access (item.price, notitem["price"]).
Send this:
{
"name": "Notebook",
"price": 4.50
}
And we get back:
{
"created": {"name": "Notebook", "description": null, "price": 4.5, "in_stock": true},
"total_with_tax": 5.31
}
How FastAPI decides “is this a body param?”
The rule of thumb in simple language:
- Param name in
{}→ path param. - Simple type (
int,str,bool, etc.) → query param. - Pydantic model → request body.
Nested models
Pydantic composes nicely. Models can hold other models:
class Address(BaseModel):
street: str
city: str
zip_code: str
class User(BaseModel):
name: str
email: str
addresses: list[Address] = []
@app.post("/users/")
async def create_user(user: User):
return user
The OpenAPI schema in /docs will show the full nested shape.
Multiple body params
We can declare more than one Pydantic param and FastAPI merges them into a single JSON body, keyed by the param names:
class Item(BaseModel):
name: str
price: float
class User(BaseModel):
username: str
@app.post("/orders/")
async def place_order(item: Item, user: User):
return {"item": item, "user": user}
Expected body:
{
"item": {"name": "Pen", "price": 2.5},
"user": {"username": "manish"}
}
Note the nesting — item and user become top-level keys. This is different from Flask where we’d flatten everything.
Body() for embedding singletons
What if we want a single scalar in the body — not a path or query param? Use Body():
from fastapi import Body
@app.post("/orders/{order_id}/notes/")
async def add_note(order_id: int, note: str = Body(...)):
return {"order_id": order_id, "note": note}
Without Body(), FastAPI would treat note: str as a query param. With it, note lives in the JSON body. Expected:
{"note": "Customer prefers afternoon delivery"}
Embedding a single Pydantic model
Normally one Pydantic param becomes the entire body. If we want to wrap it under its name (for API consistency, or to add fields later), pass embed=True:
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item = Body(..., embed=True)):
return {"item_id": item_id, "item": item}
Now the body has to look like:
{
"item": {"name": "Pen", "price": 2.5}
}
Instead of just the raw item fields. Useful when our API standardizes on {"resource_name": {...}} envelopes.
Field-level validation
For per-field rules, use Field inside the model — same vibe as Query and Path:
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0, description="Must be positive")
sku: str = Field(pattern=r"^SKU-\d{6}$")
Pydantic handles all of these before our handler sees the request.
Interview takeaway
Pydantic is the heart of FastAPI’s developer experience. We declare data shapes once as a BaseModel and get parsing, validation, serialization, type hints, and OpenAPI schema in one shot. When asked “how does FastAPI validate request bodies?” — the answer is “it delegates to Pydantic at the framework boundary.”