Pydantic — Request & Response Validation
Type-safe data models that validate at runtime with Pydantic v2 — request bodies, response schemas, and field constraints
The Problem
APIs without validation accept anything — empty strings, negative prices, SQL injection payloads. Manual validation (if not name: raise ...) is tedious, inconsistent, and easy to forget.
Pydantic models validate automatically at the boundary. Invalid data never reaches your business logic.
Define Models
# app/api/models/items.py
from pydantic import BaseModel, Field
class ItemCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
description: str | None = Field(None, max_length=1000)
price: float = Field(..., gt=0)
class ItemResponse(BaseModel):
id: str
name: str
description: str | None
price: float
class ItemListResponse(BaseModel):
items: list[ItemResponse]
total: int
What Each Part Does
Field(...) — Required with Constraints
name: str = Field(..., min_length=1, max_length=255)
...(Ellipsis) means required — no default valuemin_length=1— empty strings rejectedmax_length=255— prevents oversized inputs
Field(None) — Optional
description: str | None = Field(None, max_length=1000)
- Defaults to
Noneif not provided - If provided, still validated against
max_length
gt=0 — Numeric Constraints
price: float = Field(..., gt=0)
gt= greater than (exclusive)- Also available:
ge(>=),lt(<),le(<=)
How FastAPI Uses Models
Request Model (Input)
@router.post("/", response_model=ItemResponse, status_code=201)
async def create_item(item: ItemCreate) -> ItemResponse:
# 'item' is already validated — Pydantic parsed the JSON body
item_id = str(uuid.uuid4())
created = ItemResponse(id=item_id, **item.model_dump())
return created
FastAPI sees the item: ItemCreate type hint and:
- Reads the JSON request body
- Validates it against
ItemCreate - Returns 422 Unprocessable Entity if validation fails
- Passes the validated model to your function
Response Model (Output)
@router.get("/", response_model=ItemListResponse)
async def list_items() -> ItemListResponse:
items = list(_items.values())
return ItemListResponse(items=items, total=len(items))
response_model=ItemListResponse ensures the response matches the schema — extra fields are stripped.
Validation in Action
# Valid request
curl -X POST /api/v1/items/ -d '{"name": "Widget", "price": 9.99}'
# → 201 Created
# Empty name
curl -X POST /api/v1/items/ -d '{"name": "", "price": 9.99}'
# → 422 {"detail": [{"loc": ["body", "name"], "msg": "String should have at least 1 character"}]}
# Negative price
curl -X POST /api/v1/items/ -d '{"name": "Widget", "price": -5}'
# → 422 {"detail": [{"loc": ["body", "price"], "msg": "Input should be greater than 0"}]}
# Missing required field
curl -X POST /api/v1/items/ -d '{}'
# → 422 (lists all missing fields)
model_dump() — The Bridge
created = ItemResponse(id=item_id, **item.model_dump())
model_dump() converts the Pydantic model to a plain dict. The ** spread operator passes each key as a keyword argument. This is the Pydantic v2 replacement for v1’s .dict().
Automatic OpenAPI Schema
FastAPI generates OpenAPI schemas from your Pydantic models — these appear in the Swagger UI at /docs. No separate schema files needed.
Next Step
In the next lesson, we containerize the application with Docker — multi-stage build, non-root user, and a docker-compose dev workflow that eliminates the need for virtual environments.