Data Modeling with Pydantic
This document covers FastAPI parameters (Path, Query, Body, Header, Cookie) and data modeling with Pydantic for request/response validation.
📚 Table of Contents​
- Part 1: Path Parameters
- Part 2: Query Parameters
- Part 3: Request Body
- Part 4: Data Modeling with Pydantic
- Part 5: Header Parameters
- Part 6: Cookie Parameters
Part 1: Path Parameters​
What are Path Parameters?​
Path parameters are variables that are part of the URL path. They allow you to capture values from the URL and use them inside your function.
For example, in the URL /items/42, the value 42 can be captured as a path parameter.
Basic Example​
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
Explanation:
@app.get("/items/{item_id}"): Defines a route whereitem_idis a path parameteritem_id: int: FastAPI automatically converts the path parameter to an integer- The function returns the value of
item_idas JSON
Multiple Path Parameters​
@app.get("/users/{user_id}/items/{item_id}")
async def read_user_item(user_id: int, item_id: str):
return {"user_id": user_id, "item_id": item_id}
Path Parameter with Validation​
You can use Path() for validation:
from fastapi import Path
@app.get("/products/{product_id}")
async def get_product(
product_id: int = Path(..., gt=0, description="The ID must be greater than 0")
):
return {"product_id": product_id}
gt=0: Ensures the ID is greater than 0...: Means the parameter is required
Path Parameters vs Query Parameters​
| Type | Location | Example |
|---|---|---|
| Path | Part of URL path | /items/42 |
| Query | After the ? | /items?skip=0&limit=10 |
Part 2: Query Parameters​
1. Basic Query Parameters​
Query parameters are defined as function parameters that are not in the path.
from fastapi import FastAPI
app = FastAPI()
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
@app.get("/items/")
async def read_items(skip: int = 0, limit: int = 10):
return fake_items_db[skip : skip + limit]
| Example URL | Values |
|---|---|
http://127.0.0.1:8000/items/ | skip=0, limit=10 |
http://127.0.0.1:8000/items/?skip=20 | skip=20, limit=10 |
Optional Query Parameters​
To make a query parameter optional, set its default to None:
@app.get("/items/{item_id}")
async def read_item(item_id: str, q: str | None = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}
Boolean Type Conversion​
FastAPI automatically converts strings to booleans. Values like 1, True, true, on, yes are converted to True:
@app.get("/items/{item_id}")
async def read_item(item_id: str, q: str | None = None, short: bool = False):
item = {"item_id": item_id}
if q:
item.update({"q": q})
if not short:
item.update({"description": "This is an amazing item with a long description"})
return item
Required Query Parameters​
Declare a parameter without a default value to make it required:
@app.get("/items/{item_id}")
async def read_user_item(item_id: str, needy: str):
return {"item_id": item_id, "needy": needy}
Mixing Parameter Types​
@app.get("/items/{item_id}")
async def read_user_item(
item_id: str, # Path parameter
needy: str, # Required query parameter
skip: int = 0, # Query with default
limit: int | None = None # Optional query
):
return {"item_id": item_id, "needy": needy, "skip": skip, "limit": limit}
2. Query Parameter Validation​
FastAPI validates query parameters using type annotations and Query():
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/search/")
def search_items(
keyword: str = Query(..., min_length=3, max_length=50, regex="^[a-zA-Z0-9 ]+$")
):
return {"keyword": keyword}
Common Validation Examples:​
String Length and Pattern:
keyword: str = Query(..., min_length=3, max_length=50, regex="^[a-zA-Z0-9 ]+$")
Range Validation for Numbers:
@app.get("/ratings/")
def get_ratings(
min_rating: float = Query(0, ge=0, le=5),
max_rating: float = Query(5, ge=0, le=5)
):
return {"min_rating": min_rating, "max_rating": max_rating}
List of Values:
@app.get("/tags/")
def get_items_by_tags(tags: list[str] = Query([])):
return {"tags": tags}
# URL: /tags/?tags=python&tags=fastapi&tags=api
Using Aliases:
@app.get("/items/")
def get_items(sort_by: str = Query("name", alias="sortBy")):
return {"sort_by": sort_by}
# Accepts ?sortBy=price but maps to sort_by in function
Deprecating a Parameter:
@app.get("/legacy/")
def legacy_endpoint(old_param: str = Query(None, deprecated=True)):
return {"old_param": old_param}
3. Query Parameters with Models​
Group multiple query parameters using a Pydantic model for cleaner code:
Without a Model:
@app.get("/items/")
def get_items(skip: int = 0, limit: int = 10, search: str = None):
return {"skip": skip, "limit": limit, "search": search}
With a Query Parameter Model:
from fastapi import FastAPI, Depends
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class ItemQueryParams(BaseModel):
skip: int = 0
limit: int = 10
search: Optional[str] = None
@app.get("/items/")
def get_items(params: ItemQueryParams = Depends()):
return params
- Keeps code cleaner and more maintainable
- Enables reusability of validation logic
- Automatically generates OpenAPI documentation
- Supports default values, type checking, and validation rules
Part 3: Request Body​
1. Body Basics​
A request body is used when the client sends structured data to the server, typically in JSON format for creating or updating resources.
Basic Example Using Body:
from fastapi import FastAPI, Body
app = FastAPI()
@app.post("/users/")
def create_user(name: str = Body(...), age: int = Body(...)):
return {"name": name, "age": age}
Preferred Way - Use a Pydantic Model:
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
@app.post("/users/")
def create_user(user: User):
return user
FastAPI automatically:
- Parses the JSON body into a
Userobject - Validates the fields
- Generates OpenAPI docs
Advanced Body Options:
from fastapi import Body
@app.post("/items/")
def create_item(
name: str = Body(..., embed=True, description="The name of the item", example="Book")
):
return {"name": name}
embed=True: Wraps the value in a key (e.g.,{"name": "Book"}instead of just"Book")
2. Nested Models​
Use nested Pydantic models for complex JSON structures:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
app = FastAPI()
class Address(BaseModel):
street: str
city: str
zip_code: str
class User(BaseModel):
name: str
age: int
address: Address # Nested model
@app.post("/users/")
def create_user(user: User):
return user
Example JSON Body:
{
"name": "Alice",
"age": 30,
"address": {
"street": "123 Main St",
"city": "Hanoi",
"zip_code": "100000"
}
}
Nested Lists of Models:
class Item(BaseModel):
name: str
quantity: int
class Order(BaseModel):
order_id: str
items: List[Item]
@app.post("/orders/")
def create_order(order: Order):
return order
Example JSON Body:
{
"order_id": "ORD123",
"items": [
{ "name": "Book", "quantity": 2 },
{ "name": "Pen", "quantity": 5 }
]
}
- Clean structure for complex data
- Automatic validation of nested fields
- Better documentation in Swagger UI
- Reusability of models across endpoints
Part 4: Data Modeling with Pydantic​
1. Pydantic BaseModel​
Pydantic is the foundation for data validation in FastAPI:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Item(BaseModel):
name: str = Field(..., min_length=3, max_length=50)
description: str | None = None
price: float = Field(..., gt=0)
in_stock: bool = Field(default=True)
@app.post("/items/")
async def create_item(item: Item):
return {"received": item}
What Happens:
- FastAPI reads and parses the JSON body
- Converts it into a Pydantic model (
Item) - Validates all fields
- Returns structured JSON automatically
2. Field Validation​
Using Built-in Validation Rules:
from pydantic import BaseModel, Field
class Item(BaseModel):
name: str = Field(..., min_length=3, max_length=50)
price: float = Field(..., gt=0) # greater than 0
quantity: int = Field(default=1, ge=0, le=1000) # 0 <= quantity <= 1000
Custom Validators:
from pydantic import BaseModel, Field, field_validator
class Item(BaseModel):
name: str
price: float
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
if v is None or v.strip() == "":
raise ValueError("Name is required and cannot be empty.")
if len(v) < 3:
raise ValueError("Name must be at least 3 characters.")
if len(v) > 50:
raise ValueError("Name must be at most 50 characters.")
return v
@field_validator("price")
@classmethod
def validate_price(cls, v: float) -> float:
if v <= 0:
raise ValueError("Price must be greater than 0.")
return v
3. Response Models​
Use response_model to control output format:
from fastapi import FastAPI
from pydantic import BaseModel
from datetime import datetime
app = FastAPI()
class Item(BaseModel):
name: str
class ItemResponse(BaseModel):
name: str
date_modified: datetime
@app.post("/items/", response_model=ItemResponse)
async def create_item(item: Item):
return ItemResponse(name=item.name, date_modified=datetime.now())
Response:
{
"name": "Laptop",
"date_modified": "2026-01-07T11:58:56.752533"
}
Global Exception Handling​
Handle validation errors globally:
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={
"success": False,
"error": {
"code": "VALIDATION_ERROR",
"message": str(exc),
},
},
)
Part 5: Header Parameters​
Use Header() to extract values from HTTP request headers:
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/items/")
def read_items(user_agent: str = Header(None)):
return {"User-Agent": user_agent}
Required Header Example​
@app.get("/secure-data/")
def get_secure_data(api_key: str = Header(...)):
return {"API-Key": api_key}
Header Name Conventions​
FastAPI automatically converts underscore _ to hyphen -:
@app.get("/check/")
def check_header(x_token: str = Header(...)):
return {"X-Token": x_token}
# This reads the X-Token header from the request
Multiple Headers​
@app.get("/info/")
def get_info(
x_token: str = Header(...),
x_request_id: str = Header(None)
):
return {"token": x_token, "request_id": x_request_id}
Part 6: Cookie Parameters​
Use Cookie() to extract values from HTTP cookies:
from fastapi import FastAPI, Cookie
app = FastAPI()
@app.get("/read-cookie/")
def read_cookie(session_id: str = Cookie(None)):
return {"session_id": session_id}
Required Cookie Example​
@app.get("/secure/")
def secure_area(auth_token: str = Cookie(...)):
return {"auth_token": auth_token}
Session Authentication with Cookies​
from fastapi import FastAPI, Response, Cookie, HTTPException
app = FastAPI()
fake_session_store = {}
@app.post("/login")
def login(response: Response, username: str, password: str):
if username == "admin" and password == "secret":
session_id = "abc123" # In real apps, generate securely
fake_session_store[session_id] = username
response.set_cookie(key="session_id", value=session_id, httponly=True)
return {"message": "Logged in"}
raise HTTPException(status_code=401, detail="Invalid credentials")
@app.get("/profile")
def get_profile(session_id: str = Cookie(None)):
if session_id in fake_session_store:
return {"username": fake_session_store[session_id]}
raise HTTPException(status_code=401, detail="Invalid or missing session")
@app.post("/logout")
def logout(response: Response, session_id: str = Cookie(None)):
if session_id in fake_session_store:
del fake_session_store[session_id]
response.delete_cookie("session_id")
return {"message": "Logged out"}
response.set_cookie(...): Sets the cookie on loginCookie(...): Reads the cookie from incoming requestsresponse.delete_cookie(...): Removes the cookie on logouthttponly=True: Prevents JavaScript access to the cookie (for security)
📊 Parameter Types Summary​
| Parameter Type | Source | Use Case | Example |
|---|---|---|---|
| Path | URL path | Resource identification | /users/{id} |
| Query | URL query string | Filtering, pagination | ?search=abc&limit=10 |
| Body | Request body (JSON) | Structured data | { "name": "Alice" } |
| Header | HTTP headers | Metadata, auth, tracing | Authorization: Bearer ... |
| Cookie | HTTP cookies | Sessions, preferences | session_id=abc123 |
📖 References​
| Resource | Link | Duration |
|---|---|---|
| FastAPI Pydantic BaseModel | Body in FastAPI | ~10 min |
| Nested Models | Body - Nested Models | ~10 min |
| Response Model | Response Model | ~10 min |
| Path Parameters | Path Parameters | ~15 min |
| Query Parameters | Query Parameters | ~15 min |
Learning Checklist:​
- Define a Pydantic BaseModel for request body and verify type handling
- Launch
/docsand inspect the generated OpenAPI schemas - Create a nested BaseModel structure and test nested validation
- Add constraints using
Field(),Path(), orQuery()and verify in OpenAPI UI - Use
response_modelto ensure output filtering and correct documentation - Set
status_code,descriptionin route decorators for enhanced metadata - Test error responses resulting from schema violations
- Confirm that examples appear in both Swagger UI and ReDoc
This document covers all input parameter types in FastAPI:
- Path Parameters: Part of URL, for resource identification
- Query Parameters: After
?, for filtering and pagination - Body: JSON data for complex requests
- Header: HTTP headers for metadata and auth
- Cookie: Client-side storage for sessions
Pydantic provides powerful validation through BaseModel, Field(), and custom validators.