Pydantic Models
Schemas: Zod / interfaces → Pydantic BaseModel
Section titled “Schemas: Zod / interfaces → Pydantic BaseModel”In TypeScript you describe a request body shape with an interface or a Zod schema. In FastAPI you describe it with a Pydantic BaseModel subclass. Pass that class as a type hint on your route function argument and FastAPI will parse and validate the request body automatically.
// TypeScript — Zod schema + inferred typeimport { z } from 'zod';
const CreateItemSchema = z.object({ name: z.string().min(1), price: z.number().positive(), inStock: z.boolean().default(true), tags: z.array(z.string()).default([]),});
type CreateItem = z.infer<typeof CreateItemSchema>;
// NestJS DTO with class-validatorexport class CreateItemDto { @IsString() @MinLength(1) name: string; @IsNumber() @IsPositive() price: number; @IsBoolean() @IsOptional() inStock?: boolean = true;}# FastAPI — Pydantic BaseModelfrom pydantic import BaseModelfrom typing import Optional, List
class CreateItem(BaseModel): name: str price: float in_stock: bool = True tags: List[str] = []
# Use it in a route — FastAPI parses + validates automaticallyfrom fastapi import FastAPIapp = FastAPI()
@app.post("/items", status_code=201)async def create_item(item: CreateItem): return item # Pydantic serialises back to JSONNested models
Section titled “Nested models”Pydantic models compose naturally — embed one model inside another exactly like nested Zod objects.
// Zod nested objectconst AddressSchema = z.object({ street: z.string(), city: z.string(), zip: z.string().length(5),});
const UserSchema = z.object({ name: z.string(), address: AddressSchema,});# Pydantic nested modelsfrom pydantic import BaseModel
class Address(BaseModel): street: str city: str zip_code: str
class User(BaseModel): name: str address: Address
# FastAPI will parse this nested JSON automatically:# { "name": "Alice", "address": { "street": "1 Main St", ... } }Response models — separating input from output
Section titled “Response models — separating input from output”A common pattern is having separate models for input (what the client sends) and output (what you return). Pydantic makes this easy to compose.
// TypeScript — separate input / output typesinterface CreateItemDto { name: string; price: number; }interface ItemResponse { id: number; name: string; price: number; createdAt: string; }# Pydantic — separate input / output modelsfrom pydantic import BaseModelfrom datetime import datetime
class CreateItem(BaseModel): # Request body name: str price: float
class ItemResponse(BaseModel): # Response shape id: int name: str price: float created_at: datetime
# Route uses response_model= to filter + serializefrom fastapi import FastAPIapp = FastAPI()
@app.post("/items", response_model=ItemResponse, status_code=201)async def create_item(item: CreateItem): # Normally you'd save to DB and return the DB row return ItemResponse(id=1, name=item.name, price=item.price, created_at=datetime.utcnow())Try it — stdlib dataclasses as a runnable stand-in
Section titled “Try it — stdlib dataclasses as a runnable stand-in”Pydantic cannot run in the browser. dataclasses from the standard library gives you the same shape (typed fields, defaults, auto __init__) without validation, so you can explore the mental model here:
from dataclasses import dataclass, field, asdictfrom typing import Optional, Listimport json
@dataclassclass CreateItem: name: str price: float in_stock: bool = True tags: List[str] = field(default_factory=list) description: Optional[str] = None
# Instantiate — same feel as BaseModel(...)item = CreateItem(name="Widget", price=9.99, tags=["sale", "new"])print("item:", item)
# Serialize to dict (like .model_dump())data = asdict(item)print("dict :", data)
# Serialize to JSONprint("json :", json.dumps(data, indent=2))
# Nested dataclass@dataclassclass OrderLine: item: CreateItem quantity: int
line = OrderLine(item=item, quantity=3)print("\nnested:", asdict(line))Loading Python runtime (first run only)…