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)
| Constraint | Meaning |
|---|---|
gt | strictly greater than |
ge | greater than or equal |
lt | strictly less than |
le | less than or equal |
multiple_of | must 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
| Mode | When 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/..."
}
]
}locis a list:["body", "field_name"],["query", "param"],["path", "id"].typeis 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