Nested Models
Real APIs have hierarchical data: an order with line items, a user with addresses, a report with nested sections. Pydantic models nest naturally, and FastAPI validates the whole structure, including lists of models, in one pass.
Nested Structures
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Address(BaseModel):
street: str
city: str
zip_code: str = Field(pattern=r"^\d{5}$")
class Customer(BaseModel):
name: str
email: str
address: Address
extra_addresses: list[Address] = []
class Order(BaseModel):
customer: Customer
total: float = Field(ge=0)
@app.post("/orders/")
def create_order(order: Order):
return {"customer": order.customer.name, "total": order.total}
FastAPI validates deeply: a missing zip_code or a bad pattern in extra_addresses[1] produces a 422 pointing at that exact location.
Lists of Models
class LineItem(BaseModel):
sku: str
quantity: int = Field(gt=0)
price: float = Field(ge=0)
class Cart(BaseModel):
items: list[LineItem] = Field(min_length=1)
@property
def total(self) -> float:
return sum(i.quantity * i.price for i in self.items)
Computed Fields
Use properties or model_validator to add derived data that clients should see but never send:
from pydantic import model_validator
class OrderOut(BaseModel):
customer_name: str
total: float
currency: str = "USD"
@model_validator(mode="after")
def add_total_display(self):
self.total_display = f"{self.total:.2f} {self.currency}"
return self
Copying and Updating
model_copy(update={...}) produces a modified copy without mutation, which is handy when building response models:
out = order.customer.model_copy(update={"name": order.customer.name.upper()})
Summary
Model hierarchical payloads with nested Pydantic models: objects inside objects, lists of models, constraints at every level, and computed fields via properties or model_validator. FastAPI validates the entire tree and reports the exact failing field, so deep structures cost you nothing extra.