FastAPI Cheatsheet

Validation

Use this FastAPI reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

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