Requests & Validation — Cheat Sheet
FastAPI · 4 topics. Download the PDF or the Instagram carousel and share it.
Validation Deep Dive — Field, field_validator & model_validator
Field() handles declarative constraints (bounds, lengths, patterns); @field_validator runs custom logic per field; @model_validator checks rules across fields — all producing structured 422s.
- ✓Escalate: Field() constraints → field_validator → model_validator
- ✓Validators return the stored value — normalise (strip, title, +91) at the boundary
- ✓model_validator(mode="after") is the home for cross-field rules
- ✓Raise ValueError with a clear message; keep validators free of I/O and DB calls
from pydantic import BaseModel, Field, field_validator
class StudentIn(BaseModel):
name: str = Field(min_length=2, max_length=60)
phone: str # custom rule below
cgpa: float = Field(ge=0, le=10)
pincode: str = Field(pattern=r"^[1-9][0-9]{5}$") # Indian pincode shape
@field_validator("phone")
@classmethod
def valid_indian_mobile(cls, v: str) -> str:
digits = "".join(ch for ch in v if ch.isdigit())
if digits.startswith("91") and len(digits) == 12:
digits = digits[2:] # strip +91 → normalise
if len(digits) != 10 or digits[0] not in "6789":
raise ValueError("must be a valid 10-digit Indian mobile")
return digits # ← transformed value is stored
@field_validator("name")
@classmethod
def tidy_name(cls, v: str) -> str:
return v.strip().title() # " asha rao " → "Asha Rao"
# StudentIn(name="asha rao", phone="+91 98765 43210", cgpa=8.7, pincode="413001")
# → phone stored as "9876543210", name as "Asha Rao"
# phone="12345" → 422: {"loc": ["body", "phone"],
# "msg": "Value error, must be a valid 10-digit Indian mobile"}Nested Models — Real-World Payloads
Models compose: a field typed as another BaseModel nests an object, list[Model] nests an array, and validation runs recursively with error paths like items.1.qty — plus rich types (EmailStr, UUID, datetime) and Enums for free.
- ✓Nest by typing: field: Address, items: list[OrderItem], recursive validation free
- ✓422 loc paths walk the tree with list indexes — clients can pinpoint bad rows
- ✓EmailStr/HttpUrl/UUID/datetime/Decimal replace hand-rolled format regexes
- ✓str-Enum fields lock vocabulary and render as dropdowns in /docs
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Address(BaseModel):
line1: str
city: str
pincode: str = Field(pattern=r"^[1-9][0-9]{5}$")
class OrderItem(BaseModel):
sku: str
name: str
qty: int = Field(ge=1, le=20)
price: float = Field(gt=0)
class OrderIn(BaseModel):
customer: str
address: Address # nested object
items: list[OrderItem] = Field(min_length=1) # non-empty array
coupon: str | None = None
@app.post("/orders")
def place_order(order: OrderIn):
total = sum(i.qty * i.price for i in order.items)
return {"city": order.address.city, "items": len(order.items), "total": total}
# Payload:
# {"customer": "Ravi",
# "address": {"line1": "12 FC Road", "city": "Pune", "pincode": "411005"},
# "items": [{"sku": "S1", "name": "Kurta", "qty": 2, "price": 799},
# {"sku": "S2", "name": "Mojari", "qty": 0, "price": 1299}]}
#
# → 422: {"loc": ["body", "items", 1, "qty"],
# "msg": "Input should be greater than or equal to 1"}
# ← the SECOND item's qty, pinpointed. Frontend can highlight that row.Forms & File Uploads — multipart/form-data
Form() reads HTML-form fields, File()/UploadFile handle uploads — UploadFile streams to a spooled temp file so big files don't eat RAM. Validate content type and size yourself; the framework won't.
- ✓python-multipart required; Form() = form fields, File()/UploadFile = uploads
- ✓UploadFile spools to disk with async read/seek — prefer it over bytes
- ✓filename and content_type come from the client — verify magic bytes yourself
- ✓Cap size while streaming; store under generated names, never client names
from fastapi import FastAPI, File, Form, UploadFile
app = FastAPI()
# pip install python-multipart ← required for forms & uploads
@app.post("/login") # classic form post (no file)
def login(username: str = Form(), password: str = Form()):
return {"user": username} # OAuth2 password flow uses exactly this
@app.post("/marksheets") # fields + file in ONE request
async def upload_marksheet(
student_id: int = Form(),
semester: int = Form(ge=1, le=8),
remarks: str = Form(default=""),
sheet: UploadFile = File(),
):
header = await sheet.read(5) # first bytes (magic number)
await sheet.seek(0) # rewind before the real read
content = await sheet.read()
return {
"student": student_id,
"semester": semester,
"filename": sheet.filename, # client-supplied — do NOT trust
"declared_type": sheet.content_type, # also client-supplied
"size_bytes": len(content),
"is_pdf": header == b"%PDF-", # verified from actual bytes
}
# bytes vs UploadFile:
# photo: bytes = File() → whole file in RAM (fine for tiny files only)
# photo: UploadFile → spooled temp file, async streaming — DEFAULT
# Multiple files: photos: list[UploadFile] = File()Headers, Cookies & the Request Object
Header() and Cookie() bind request metadata as typed parameters (X-API-Key becomes x_api_key); Response.set_cookie sends session cookies with the security flags that interviews love; Request is the escape hatch.
- ✓Header()/Cookie() bind metadata as typed params; x_api_key ↔ X-Api-Key mapping is automatic
- ✓set_cookie with httponly + secure + samesite="lax" is the baseline session recipe
- ✓Request is the escape hatch: url, method, headers, client
- ✓Behind a proxy, the client IP is in X-Forwarded-For — and only trustworthy from your own proxy
from fastapi import FastAPI, Header, Cookie, HTTPException
app = FastAPI()
@app.get("/profile")
def profile(
x_api_key: str = Header(), # required: X-Api-Key
user_agent: str | None = Header(default=None), # standard header, optional
x_request_id: str | None = Header(default=None), # tracing id from gateway
session_id: str | None = Cookie(default=None), # cookie by name
):
if x_api_key != "tophub-secret-1": # (real auth → Auth chapters)
raise HTTPException(401, "invalid API key")
return {
"client": user_agent,
"trace": x_request_id,
"has_session": session_id is not None,
}
# curl http://127.0.0.1:8000/profile \
# -H "X-Api-Key: tophub-secret-1" \
# -H "X-Request-ID: req-7f3a" \
# --cookie "session_id=abc123"
# Notes:
# - underscore ↔ hyphen conversion is automatic (convert_underscores=True)
# - headers are case-insensitive per HTTP spec
# - Authorization: Bearer <token> is usually read via Security utilities
# (OAuth2PasswordBearer) rather than raw Header() — coming in Auth