Cheatsheet for Pydantic patterns in FastAPI.
Read / Create / Update trio
class UserBase(BaseModel):
email: EmailStr
name: str = Field(..., min_length=1, max_length=120)
class UserCreate(UserBase):
password: str = Field(..., min_length=8)
class UserUpdate(BaseModel):
email: EmailStr | None = None
name: str | None = Field(default=None, min_length=1, max_length=120)
class UserRead(UserBase):
id: int
created_at: datetime
model_config = {"from_attributes": True}
Distinct shapes per operation.
PATCH partial update
@app.patch("/users/{id}", response_model=UserRead)
async def update(id: int, data: UserUpdate, db = Depends(get_db)):
user = await db.get(User, id)
if not user: raise HTTPException(404)
# exclude_unset → only fields client sent
for k, v in data.model_dump(exclude_unset=True).items():
setattr(user, k, v)
await db.commit()
return user
Query class
class PostFilter(BaseModel):
q: str | None = None
author_id: int | None = None
tag: str | None = None
created_after: datetime | None = None
@app.get("/posts", response_model=list[PostRead])
async def posts(f: PostFilter = Depends(), db = Depends(get_db)):
stmt = select(Post)
if f.q is not None: stmt = stmt.where(Post.title.ilike(f"%{f.q}%"))
if f.author_id is not None: stmt = stmt.where(Post.author_id == f.author_id)
# ...
return (await db.execute(stmt)).scalars().all()
Pagination class
class Pagination(BaseModel):
page: int = Field(1, ge=1)
limit: int = Field(20, ge=1, le=100)
@property
def offset(self) -> int:
return (self.page - 1) * self.limit
@app.get("/users", response_model=list[UserRead])
async def list_(p: Pagination = Depends(), db = Depends(get_db)):
stmt = select(User).limit(p.limit).offset(p.offset)
return (await db.execute(stmt)).scalars().all()
Generic Page envelope
from typing import TypeVar, Generic
T = TypeVar("T")
class Page(BaseModel, Generic[T]):
items: list[T]
next_cursor: str | None = None
has_more: bool
@app.get("/users", response_model=Page[UserRead])
async def list_(...):
return Page(items=[...], next_cursor=None, has_more=False)
Validation in request
class TransferRequest(BaseModel):
from_account: int
to_account: int
amount: Decimal = Field(..., gt=0)
@model_validator(mode="after")
def different_accounts(self):
if self.from_account == self.to_account:
raise ValueError("from and to must differ")
return self
Webhook signature verify (validator)
class WebhookEnvelope(BaseModel):
payload: dict
signature: str
@model_validator(mode="after")
def verify_sig(self):
expected = hmac.new(SECRET, json.dumps(self.payload).encode(), "sha256").hexdigest()
if not hmac.compare_digest(self.signature, expected):
raise ValueError("bad signature")
return self
Form + body
class UploadMeta(BaseModel):
title: str
is_public: bool
@app.post("/upload")
async def upload(
file: UploadFile,
meta: UploadMeta = Depends(), # form fields, not JSON
):
...
Or use Form() per field.
Settings as Depends
@lru_cache
def get_settings() -> Settings:
return Settings()
@app.get("/info")
async def info(s: Settings = Depends(get_settings)):
return {"env": s.env}
ORM → response model
@app.get("/users/{id}", response_model=UserRead)
async def get_user(id: int, db = Depends(get_db)):
return await db.get(User, id) # SA → UserRead (via from_attributes)
Conditional fields by user role
Better: use separate response models per role:
class UserPublic(BaseModel):
id: int
name: str
class UserPrivate(UserPublic):
email: str
settings: dict
# Endpoint returns the matching one based on auth
Examples in OpenAPI
class UserCreate(BaseModel):
email: EmailStr
name: str
model_config = {
"json_schema_extra": {
"examples": [
{"email": "[email protected]", "name": "Alice"},
]
}
}
Swagger UI shows ready-to-try examples.
Common mistakes
- Returning SA model without
response_model— sensitive fields leak. UserUpdatewith required fields — clients must send everything.- Reusing model for input and output — internal columns leak; or output too strict.
- Missing
from_attributes=Trueon Read model when source is ORM.
Read this next
If you want my Read/Create/Update + Page generic patterns, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .