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)
| 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