FastAPI Request Data Handling - Advanced Body and Validation
The Story: Building a Smart Pizza Order System
Imagine you’re building a magic pizza ordering machine. Customers tell the machine what they want, and it needs to understand everything perfectly—the pizza type, toppings, delivery address, and special instructions. If something’s wrong (like ordering -5 pizzas), the machine should politely say “Oops, that doesn’t work!”
That’s exactly what FastAPI’s advanced body handling and validation does for your web apps!
1. Multiple Body Parameters
The Story
Think of ordering at a restaurant. You don’t just say “food please!” You give the waiter:
- Your main dish (the pizza)
- Your drink (the beverage)
- Dessert (something sweet)
Each is a separate thing, but they all go in one order.
What It Means
FastAPI lets you receive multiple different objects in a single request body. Each object gets its own model!
Simple Example
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Pizza(BaseModel):
name: str
size: str
class Drink(BaseModel):
name: str
ice: bool
@app.post("/order")
async def create_order(
pizza: Pizza,
drink: Drink
):
return {
"pizza": pizza,
"drink": drink
}
What the Request Looks Like
{
"pizza": {
"name": "Margherita",
"size": "large"
},
"drink": {
"name": "Cola",
"ice": true
}
}
The Magic
FastAPI automatically:
- Knows to look for a
pizzakey - Knows to look for a
drinkkey - Validates each separately!
graph TD A["Request Body"] --> B{FastAPI Parser} B --> C["pizza object"] B --> D["drink object"] C --> E["Validate Pizza"] D --> F["Validate Drink"] E --> G["Return Response"] F --> G
2. Embedded Body
The Story
Sometimes your pizza order has extra details inside details. Like:
- Pizza → has → Crust Details → has → Thickness, Stuffed?
It’s like a Russian nesting doll - objects inside objects!
What It Means
You can nest models inside other models. FastAPI handles all the layers automatically.
Simple Example
from pydantic import BaseModel
class Crust(BaseModel):
type: str
thickness: str
class Pizza(BaseModel):
name: str
crust: Crust # Embedded!
@app.post("/pizza")
async def make_pizza(pizza: Pizza):
return pizza
What the Request Looks Like
{
"name": "Supreme",
"crust": {
"type": "thin",
"thickness": "5mm"
}
}
Using Body(embed=True)
Want to wrap a single model in an extra layer?
from fastapi import Body
@app.post("/item")
async def create_item(
item: Item = Body(embed=True)
):
return item
Without embed:
{"name": "Pizza", "price": 10}
With embed:
{
"item": {"name": "Pizza", "price": 10}
}
3. Partial Updates with PATCH
The Story
Your friend ordered a pizza but wants to change just the size—not the whole order!
- PUT = “Replace everything”
- PATCH = “Change only what I mention”
It’s like editing a document. Sometimes you fix one typo, not rewrite the whole thing!
Simple Example
from typing import Optional
from pydantic import BaseModel
class PizzaUpdate(BaseModel):
name: Optional[str] = None
size: Optional[str] = None
price: Optional[float] = None
# Fake database
pizzas_db = {
1: {"name": "Margherita", "size": "M", "price": 12.0}
}
@app.patch("/pizza/{pizza_id}")
async def update_pizza(
pizza_id: int,
pizza: PizzaUpdate
):
stored = pizzas_db[pizza_id]
update_data = pizza.model_dump(exclude_unset=True)
for key, value in update_data.items():
stored[key] = value
return stored
The Magic: exclude_unset=True
This is the secret sauce! It means:
- Only get fields the user actually sent
- Ignore fields they didn’t mention
Request:
{"size": "XL"}
Result: Only size changes. Name and price stay the same!
graph TD A["Original: name=Margherita, size=M, price=12"] --> B["PATCH Request: size=XL"] B --> C["exclude_unset=True"] C --> D["Only update: size"] D --> E["Result: name=Margherita, size=XL, price=12"]
4. Validation Constraints
The Story
Remember our pizza machine? It needs rules:
- You can’t order 0 pizzas
- Pizza name can’t be empty
- Price can’t be negative
These are validation constraints—guardrails that keep data clean!
Common Constraints
from pydantic import BaseModel, Field
class Pizza(BaseModel):
# String constraints
name: str = Field(
min_length=1, # At least 1 character
max_length=50 # At most 50 characters
)
# Number constraints
price: float = Field(
gt=0, # Greater than 0
le=1000 # Less than or equal to 1000
)
# Integer constraints
quantity: int = Field(
ge=1, # Greater or equal to 1
lt=100 # Less than 100
)
Constraint Cheat Sheet
| Constraint | Meaning | Example |
|---|---|---|
gt |
Greater than | gt=0 → must be > 0 |
ge |
Greater or equal | ge=1 → must be >= 1 |
lt |
Less than | lt=100 → must be < 100 |
le |
Less or equal | le=50 → must be <= 50 |
min_length |
Minimum string length | min_length=3 |
max_length |
Maximum string length | max_length=100 |
pattern |
Regex pattern | pattern="^[A-Z]" |
Real Example
from pydantic import BaseModel, Field, field_validator
class Order(BaseModel):
customer_name: str = Field(min_length=2)
email: str = Field(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+quot;)
items: int = Field(ge=1, le=10)
total: float = Field(gt=0)
@field_validator('customer_name')
@classmethod
def name_must_be_alpha(cls, v):
if not v.replace(' ', '').isalpha():
raise ValueError('Name must contain only letters')
return v.title()
What Happens When Validation Fails?
FastAPI returns a 422 Unprocessable Entity error with details:
{
"detail": [
{
"loc": ["body", "price"],
"msg": "Input should be greater than 0",
"type": "greater_than"
}
]
}
5. Parameter Models
The Story
Instead of listing every query parameter separately, you can group them into a model. It’s like having a form with sections instead of random questions!
The Old Way (Messy)
@app.get("/pizzas")
async def search_pizzas(
name: str = None,
min_price: float = None,
max_price: float = None,
size: str = None,
vegetarian: bool = None
):
# Gets messy with many params!
pass
The New Way (Clean!)
from typing import Annotated
from fastapi import Query
class PizzaFilter(BaseModel):
name: str | None = None
min_price: float | None = Field(None, ge=0)
max_price: float | None = Field(None, le=1000)
size: str | None = None
vegetarian: bool | None = None
model_config = {"extra": "forbid"}
@app.get("/pizzas")
async def search_pizzas(
filters: Annotated[PizzaFilter, Query()]
):
return {"filters": filters}
Why This Is Better
- Organized - All filter params in one place
- Reusable - Use same model in multiple endpoints
- Validated - Constraints apply automatically
- Documented - OpenAPI docs show it clearly
graph TD A["Query Parameters"] --> B["Parameter Model"] B --> C["Validation"] C --> D["Type Conversion"] D --> E["Your Function"] B --> F["Reuse Anywhere"] B --> G["Auto Documentation"]
Full Example
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from typing import Annotated
app = FastAPI()
class SearchParams(BaseModel):
q: str | None = Field(
None,
min_length=1,
description="Search query"
)
page: int = Field(
1,
ge=1,
description="Page number"
)
limit: int = Field(
10,
ge=1,
le=100,
description="Items per page"
)
@app.get("/search")
async def search(
params: Annotated[SearchParams, Query()]
):
return {
"query": params.q,
"page": params.page,
"limit": params.limit
}
Putting It All Together
Here’s a complete example using everything we learned:
from fastapi import FastAPI, Body
from pydantic import BaseModel, Field
from typing import Optional, Annotated
app = FastAPI()
# Embedded model
class Address(BaseModel):
street: str = Field(min_length=5)
city: str
zip_code: str = Field(pattern=r"^\d{5}quot;)
# Main model with validation
class Customer(BaseModel):
name: str = Field(min_length=2, max_length=50)
email: str
address: Address # Embedded!
class OrderItem(BaseModel):
pizza_name: str
quantity: int = Field(ge=1, le=10)
price: float = Field(gt=0)
# Multiple body parameters
@app.post("/complete-order")
async def create_order(
customer: Customer,
items: list[OrderItem]
):
total = sum(item.price * item.quantity for item in items)
return {
"customer": customer,
"items": items,
"total": total
}
# Partial update model
class OrderUpdate(BaseModel):
status: Optional[str] = None
notes: Optional[str] = None
@app.patch("/order/{order_id}")
async def update_order(
order_id: int,
update: OrderUpdate
):
changes = update.model_dump(exclude_unset=True)
return {"order_id": order_id, "updated": changes}
Key Takeaways
| Concept | What It Does | When to Use |
|---|---|---|
| Multiple Body Params | Accept several objects in one request | Complex forms with different sections |
| Embedded Body | Nest models inside models | Structured data with sub-objects |
| PATCH Updates | Change only specific fields | Edit operations |
| Validation Constraints | Enforce data rules | Always! Keep data clean |
| Parameter Models | Group query params | Search/filter endpoints |
You Did It!
You now understand how FastAPI handles complex request data like a pro! Think of it this way:
- Multiple Body = Different boxes in one delivery
- Embedded = Boxes inside boxes
- PATCH = Edit just one thing
- Validation = Rules that keep everything correct
- Parameter Models = Organized forms
Go build something amazing! Your pizza machine awaits!
