Pydantic Models and Validation
Introduction
Pydantic models define the shape and rules of your data. FastAPI uses them for JSON bodies, query structures, and response filtering. This chapter covers Pydantic v2 basics, validators, and the common pattern of separate Create / Read / Update schemas.
Prerequisites
- Path, Query, and Request Body
- Basic Python type hints
BaseModel Basics
from pydantic import BaseModel, Field
class ItemCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=200)
description: str | None = None
price: float = Field(..., gt=0)
tax: float | None = NoneCode explanation:
...inFieldmeans requiredstr | None = Noneis optional (Python 3.10+ syntax)
Use in route:
@app.post("/items")
def create_item(item: ItemCreate):
total = item.price + (item.tax or 0)
return {"name": item.name, "total": total}Separate Input and Output Schemas
Never return password hashes or internal IDs you did not intend to expose:
class UserCreate(BaseModel):
username: str
password: str
class UserRead(BaseModel):
id: int
username: str
@app.post("/users", response_model=UserRead)
def create_user(user: UserCreate):
# hash password, save to DB (later chapters)
return UserRead(id=1, username=user.username)Code explanation:
UserCreateacceptspasswordUserReadomitspasswordfrom API responses
model_config for ORM (from_attributes)
When reading SQLAlchemy objects (later):
from pydantic import ConfigDict
class ItemRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
price: floatEnables ItemRead.model_validate(db_item) from ORM instances.
Nested Models
class Image(BaseModel):
url: str
alt: str | None = None
class ItemCreate(BaseModel):
name: str
images: list[Image] = []JSON:
{
"name": "Camera",
"images": [{"url": "https://example.com/a.jpg", "alt": "Front"}]
}Field Validators (Pydantic v2)
Model validator (cross-field):
from pydantic import model_validator
class Range(BaseModel):
start: int
end: int
@model_validator(mode="after")
def check_range(self):
if self.end < self.start:
raise ValueError("end must be >= start")
return selfEmail and Constrained Types
pip install email-validatorfrom pydantic import EmailStr
class UserCreate(BaseModel):
email: EmailStr
username: strPartial Updates (PATCH)
class ItemUpdate(BaseModel):
name: str | None = None
price: float | None = Field(default=None, gt=0)
@app.patch("/items/{item_id}")
def patch_item(item_id: int, data: ItemUpdate):
updates = data.model_dump(exclude_unset=True)
return {"item_id": item_id, "updates": updates}Code explanation:
exclude_unset=Truereturns only fields the client sent- Ideal for PATCH semantics
model_dump and JSON
item = ItemCreate(name="Book", price=19.99)
item.model_dump()
item.model_dump_json()Pydantic v1 used .dict()—use model_dump() in v2.
Tip
Keep Schemas in schemas/ Package
Split schemas/user.py, schemas/item.py as projects grow—routes stay thin.
FAQ
ValidationError vs HTTP 422?
FastAPI converts Pydantic validation failures to 422 responses automatically.
Optional list default mutable?
Use Field(default_factory=list) instead of = [] for safety.
Decimal for money?
Use Decimal type with JSON encoder config or store cents as int.
Union types?
str | int works—document in OpenAPI; clients must send consistent types.
Same model for create and read?
Small demos OK—production should split schemas.
Pydantic v1 tutorials online?
Replace class Config: with model_config = ConfigDict(...) and .dict() with model_dump().