Cheatsheet for request validation. Long-form: Textbook Ch 3 and the Pydantic v2 textbook .
Basic models
from pydantic import BaseModel, Field, EmailStr
from datetime import datetime
class UserCreate(BaseModel):
email: EmailStr
full_name: str = Field(..., min_length=1, max_length=120)
age: int | None = Field(default=None, ge=0, le=150)
tags: list[str] = Field(default_factory=list, max_length=10)
@app.post("/users")
async def create(user: UserCreate):
return user
Field options
Field(
default=..., # required if Ellipsis; else default
default_factory=list,
title="...",
description="...",
examples=["[email protected]"],
alias="emailAddr",
validation_alias="email",
serialization_alias="emailAddr",
deprecated=True,
min_length=1, max_length=120,
ge=0, gt=-1, le=150, lt=151,
multiple_of=10,
pattern=r"^[a-z]+$",
json_schema_extra={"example": "..."},
exclude=True, # serialization-only
init=False, # not in __init__
repr=False, # not in repr
)
Annotated types (reusable)
from typing import Annotated
from pydantic import StringConstraints
Username = Annotated[str, StringConstraints(min_length=3, max_length=32, pattern=r"^[a-z0-9_]+$")]
class UserCreate(BaseModel):
username: Username
Field validators
from pydantic import field_validator, ValidationInfo
class M(BaseModel):
email: str
age: int
@field_validator("email", mode="before")
@classmethod
def lower(cls, v: str) -> str:
return v.lower().strip() if isinstance(v, str) else v
@field_validator("age")
@classmethod
def positive(cls, v: int) -> int:
if v < 0:
raise ValueError("age must be non-negative")
return v
@field_validator("email")
@classmethod
def cross(cls, v, info: ValidationInfo):
# info.data has fields validated so far in declaration order
return v
Cross-field validators
from pydantic import model_validator
class Order(BaseModel):
qty: int
price: float
@model_validator(mode="after")
def total_under_limit(self):
if self.qty * self.price > 1_000_000:
raise ValueError("too large")
return self
mode="before": receives raw dict input.
Discriminated unions (faster + clearer errors)
from typing import Literal, Annotated
from pydantic import Field
class Cat(BaseModel):
kind: Literal["cat"]; meows: int
class Dog(BaseModel):
kind: Literal["dog"]; barks: int
Animal = Annotated[Cat | Dog, Field(discriminator="kind")]
class Pet(BaseModel):
name: str
animal: Animal
Strict mode
class M(BaseModel):
model_config = {"strict": True}
age: int
# Or per field
from pydantic import StrictInt
class M(BaseModel):
age: StrictInt
For FastAPI request bodies: keep lax (default). Query params arrive as strings; need coercion.
Custom validation error
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
@app.exception_handler(RequestValidationError)
async def err(req, exc: RequestValidationError):
return JSONResponse(
status_code=400,
content={
"error": "validation_error",
"details": [
{"field": ".".join(str(x) for x in e["loc"][1:]), "message": e["msg"]}
for e in exc.errors()
],
},
)
Nested
class Address(BaseModel):
street: str; city: str
class UserCreate(BaseModel):
name: str
addresses: list[Address]
Errors include the path: ("body", "addresses", 0, "street").
Aliases at the boundary
class UserCreate(BaseModel):
full_name: str = Field(alias="fullName")
model_config = {"populate_by_name": True}
# JSON {"fullName": "Alice"} or {"full_name": "Alice"} both accepted
Forbid extra fields
class M(BaseModel):
model_config = {"extra": "forbid"}
name: str
forbid: unknown keys → 422. ignore (default), allow are the alternatives.
Settings (config)
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
database_url: str
secret_key: str
model_config = SettingsConfigDict(env_file=".env", env_prefix="MYAPP_")
from functools import lru_cache
@lru_cache
def get_settings() -> Settings: return Settings()
Common patterns
# Pagination
class Pagination(BaseModel):
page: int = Field(1, ge=1, le=10000)
limit: int = Field(20, ge=1, le=100)
@app.get("/users")
async def list_(p: Pagination = Depends()): ...
# Filters as a class
class PostFilter(BaseModel):
q: str | None = None
author: int | None = None
tag: str | None = None
@app.get("/posts")
async def posts(f: PostFilter = Depends()): ...
File + form combined
@app.post("/upload")
async def up(
file: UploadFile,
title: str = Form(...),
is_public: bool = Form(False),
): ...
Read this next
If you want my Pydantic + FastAPI patterns library, 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 .