Validation
Validation is automatic
Section titled “Validation is automatic”In Express you choose a validation library (Joi, Zod, class-validator) and wire it in manually. In FastAPI, validation is not optional — it runs automatically for every request the moment you declare a typed parameter. If validation fails, FastAPI returns a 422 Unprocessable Entity with a structured error body before your handler function ever runs.
Basic type constraints with Field
Section titled “Basic type constraints with Field”pydantic.Field is the equivalent of Zod’s .min(), .max(), .regex(), or NestJS’s @IsEmail(), @Min() decorators.
// Zodconst CreateItemSchema = z.object({ name: z.string().min(1).max(100), price: z.number().positive().max(99999), quantity: z.number().int().min(0).default(1), email: z.string().email().optional(), slug: z.string().regex(/^[a-z0-9-]+$/).optional(),});
// class-validator (NestJS)export class CreateItemDto { @IsString() @MinLength(1) @MaxLength(100) name: string; @IsNumber() @IsPositive() @Max(99999) price: number; @IsInt() @Min(0) @IsOptional() quantity?: number = 1; @IsEmail() @IsOptional() email?: string;}# Pydantic with Field constraintsfrom pydantic import BaseModel, Field, EmailStrfrom typing import Optional
class CreateItem(BaseModel): name: str = Field(min_length=1, max_length=100) price: float = Field(gt=0, le=99999) quantity: int = Field(default=1, ge=0) email: Optional[EmailStr] = None # needs: pip install pydantic[email] slug: Optional[str] = Field( default=None, pattern=r"^[a-z0-9-]+$" )Field constraint cheat-sheet:
Pydantic Field kwarg | Meaning | Zod equivalent |
|---|---|---|
gt=0 | greater than | .positive() |
ge=0 | greater than or equal | .min(0) |
lt=100 | less than | .max(99) |
le=100 | less than or equal | .max(100) |
min_length=1 | string min length | .min(1) |
max_length=100 | string max length | .max(100) |
pattern=r"..." | regex | .regex(...) |
The 422 error response
Section titled “The 422 error response”When validation fails, FastAPI returns a structured JSON body automatically. There is no middleware to add and no try/catch to write.
// Zod manual wiringapp.post('/items', (req, res) => { const result = CreateItemSchema.safeParse(req.body); if (!result.success) { return res.status(422).json({ errors: result.error.flatten() }); } // ...});# FastAPI — automatic 422, no code neededfrom fastapi import FastAPIfrom pydantic import BaseModel, Field
app = FastAPI()
class CreateItem(BaseModel): name: str = Field(min_length=1) price: float = Field(gt=0)
@app.post("/items")async def create_item(item: CreateItem): return item # only reached if validation passed
# POST /items {} returns:# {# "detail": [# {"loc": ["body","name"], "msg":"Field required", "type":"missing"},# {"loc": ["body","price"],"msg":"Field required", "type":"missing"}# ]# }Try it — manual validation matching Pydantic’s style
Section titled “Try it — manual validation matching Pydantic’s style”Run this locally with FastAPI for real validation. The snippet below demonstrates the same logic using only the stdlib so you can run it in the browser.
from dataclasses import dataclassfrom typing import Optionalimport re
@dataclassclass ValidationError: field: str message: str
def validate_item(data: dict) -> list: errors = []
# name: required, 1-100 chars name = data.get("name", "") if not name: errors.append(ValidationError("name", "Field required")) elif len(name) > 100: errors.append(ValidationError("name", "max_length=100"))
# price: required, must be > 0 price = data.get("price") if price is None: errors.append(ValidationError("price", "Field required")) elif not isinstance(price, (int, float)) or price <= 0: errors.append(ValidationError("price", "gt=0"))
# quantity: optional, must be >= 0 integer qty = data.get("quantity", 1) if not isinstance(qty, int) or qty < 0: errors.append(ValidationError("quantity", "ge=0"))
# slug: optional regex slug = data.get("slug") if slug and not re.match(r"^[a-z0-9-]+$", slug): errors.append(ValidationError("slug", "pattern mismatch"))
return errors
tests = [ {"name": "Widget", "price": 9.99, "quantity": 2, "slug": "widget-pro"}, {}, {"name": "X", "price": -5, "quantity": -1, "slug": "BAD SLUG"},]
for t in tests: errs = validate_item(t) if errs: print(f"INVALID {t}") for e in errs: print(f" [{e.field}] {e.message}") else: print(f"VALID {t}")Loading Python runtime (first run only)…