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 .