FastAPI Cheatsheet

Validation

Use this FastAPI reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

How FastAPI Validates

FastAPI uses Pydantic v2 for all validation. Path/query params use annotated-types-style constraints via Path(), Query(), Field(). Failures automatically return 422 Unprocessable Entity with a detailed error body — no try/except needed.

Numeric Constraints

from fastapi import Query, Path
from pydantic import BaseModel, Field

# In Query / Path:
@app.get("/items/{id}")
def get(
    id: int = Path(..., gt=0, le=9999),
    price: float = Query(default=None, ge=0.0, lt=1_000_000),
):
    ...

# In Pydantic models:
class Item(BaseModel):
    quantity: int = Field(..., ge=0)
    price: float = Field(..., gt=0)
    discount: float = Field(0.0, ge=0.0, le=1.0)
    multiple: int = Field(..., multiple_of=5)
ConstraintMeaning
gtstrictly greater than
gegreater than or equal
ltstrictly less than
leless than or equal
multiple_ofmust divide evenly

String Constraints

from pydantic import BaseModel, Field
from fastapi import Query

@app.get("/search")
def search(q: str = Query(..., min_length=2, max_length=50, pattern=r"^\w[\w\s]*$")):
    ...

class User(BaseModel):
    username: str = Field(..., min_length=3, max_length=20, pattern=r"^[a-zA-Z0-9_]+$")
    email: str     # use EmailStr for real validation

EmailStr and Other Special Types

# pip install email-validator
from pydantic import BaseModel, EmailStr, AnyHttpUrl, AnyUrl, IPvAnyAddress
from uuid import UUID
from datetime import date, datetime

class User(BaseModel):
    email: EmailStr
    website: AnyHttpUrl
    api_endpoint: AnyUrl
    ip: IPvAnyAddress
    id: UUID
    dob: date
    created_at: datetime

Annotated — Reusable Constrained Types

from typing import Annotated
from pydantic import Field
from fastapi import Query

# Define once, reuse everywhere:
PositiveInt = Annotated[int, Field(gt=0)]
ShortStr   = Annotated[str, Field(max_length=100)]

class Item(BaseModel):
    quantity: PositiveInt
    name: ShortStr

# Works in query params too:
@app.get("/items")
def list_items(limit: Annotated[int, Query(ge=1, le=100)] = 10):
    ...

Field Validators (Pydantic v2)

from pydantic import BaseModel, field_validator

class Item(BaseModel):
    name: str
    tags: list[str] = []

    @field_validator("name")
    @classmethod
    def name_not_blank(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("name must not be blank")
        return v.strip()

    @field_validator("tags", mode="before")
    @classmethod
    def split_tags(cls, v):
        # accept comma-separated string OR list
        if isinstance(v, str):
            return [t.strip() for t in v.split(",")]
        return v

mode options for @field_validator

ModeWhen it runs
"before"before Pydantic type coercion
"after" (default)after type coercion (value is already typed)
"wrap"full control; receives a handler callable
"plain"replaces default validation entirely

Model Validators

from pydantic import BaseModel, model_validator
from typing import Self

class DateRange(BaseModel):
    start: date
    end: date

    @model_validator(mode="after")
    def end_after_start(self) -> Self:
        if self.end <= self.start:
            raise ValueError("end must be after start")
        return self

    @model_validator(mode="before")
    @classmethod
    def coerce_strings(cls, data: dict) -> dict:
        # raw dict before any field parsing
        return data

Custom Types with __get_validators__ / Annotated

from pydantic import GetCoreSchemaHandler
from pydantic_core import core_schema
from typing import Any

class UpperStr(str):
    @classmethod
    def __get_pydantic_core_schema__(cls, source, handler: GetCoreSchemaHandler):
        return core_schema.no_info_after_validator_function(
            cls._validate,
            core_schema.str_schema(),
        )

    @classmethod
    def _validate(cls, v: str) -> "UpperStr":
        return cls(v.upper())

class Item(BaseModel):
    code: UpperStr   # always uppercased on parse

Extra Fields Policy

from pydantic import BaseModel, ConfigDict

class Strict(BaseModel):
    model_config = ConfigDict(extra="forbid")   # 422 if unknown keys arrive
    name: str

class Lenient(BaseModel):
    model_config = ConfigDict(extra="ignore")   # silently drop extra (default)
    name: str

class Permissive(BaseModel):
    model_config = ConfigDict(extra="allow")    # store extra in __pydantic_extra__
    name: str

Strict Mode (no coercion)

from pydantic import BaseModel, ConfigDict

class StrictItem(BaseModel):
    model_config = ConfigDict(strict=True)
    count: int     # "3" (string) will FAIL — no auto coercion

Validation Errors — Shape of the 422 Response

{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "name"],
      "msg": "Field required",
      "input": {},
      "url": "https://errors.pydantic.dev/..."
    }
  ]
}
  • loc is a list: ["body", "field_name"], ["query", "param"], ["path", "id"].
  • type is a Pydantic error code string ("string_too_short", "greater_than", etc.).

Catching ValidationError Manually

from pydantic import ValidationError

try:
    item = Item.model_validate(raw_dict)
except ValidationError as e:
    errors = e.errors()   # list of error dicts
    print(e.error_count())

Request-Level Validation vs Route-Level

# Route body → Pydantic model → automatic 422 on failure

# If you need custom 400 logic, raise HTTPException yourself:
from fastapi import HTTPException

@app.post("/items")
def create(item: Item):
    if db.exists(item.name):
        raise HTTPException(status_code=400, detail="Item already exists")
    return item

Validate Response Data Too

# response_model triggers output validation — strips extra fields,
# coerces types, and returns a clean schema-conforming JSON:
@app.get("/items/{id}", response_model=ItemOut)
def get_item(id: int):
    return db.get(id)   # extra fields in the DB row are stripped