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.
  • UserUpdate with required fields — clients must send everything.
  • Reusing model for input and output — internal columns leak; or output too strict.
  • Missing from_attributes=True on 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 .