Nested Models

Difficulty: Intermediate

Real-world APIs rarely deal with flat data structures. Most data is hierarchical - a user has an address, an order has line items, a blog post has comments. Pydantic supports nested models naturally: you can use one Pydantic model as a field type in another model, creating complex, validated data structures.

When you nest models, Pydantic validates the entire structure recursively. If the outer model is valid but a nested model has invalid data, the entire request is rejected with a detailed error pointing to the exact location of the problem. This deep validation happens automatically without any manual checking.

You can nest models to any depth. An Order might contain a list of OrderItem models, each containing a Product model. Pydantic handles this recursively. Lists of models use list[ModelName], and optional nested models use ModelName | None = None.

Model composition is a key design pattern. Instead of creating one giant model, break your data into small, focused models and compose them. An Address model can be reused in both UserProfile and Company models. A Timestamp model with created_at and updated_at can be included wherever timestamps are needed. This promotes code reuse and keeps each model simple.

FastAPI generates complete JSON Schema for nested models, including all sub-models. The Swagger UI shows the full structure with expandable sections for each nested model. Clients can see the exact expected format without guessing.

For responses, nested models are serialized recursively. Returning a Pydantic model with nested models produces properly nested JSON. FastAPI's response_model parameter validates the entire response structure, including nested models, before sending it to the client.

Code examples

Basic Nested Models

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Address(BaseModel):
    street: str
    city: str
    state: str
    zip_code: str

class UserCreate(BaseModel):
    name: str
    email: str
    address: Address

@app.post("/users")
def create_user(user: UserCreate):
    return {
        "name": user.name,
        "city": user.address.city,
        "full_address": f"{user.address.street}, {user.address.city}, {user.address.state}",
    }

# POST /users
# Body:
# {
#   "name": "Alice",
#   "email": "alice@example.com",
#   "address": {
#     "street": "123 Main St",
#     "city": "Springfield",
#     "state": "IL",
#     "zip_code": "62701"
#   }
# }

The Address model is used as a field type in UserCreate. When FastAPI receives the request, it validates both the outer user fields and the nested address fields. You access nested values with dot notation: user.address.city.

Lists of Nested Models

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class OrderItem(BaseModel):
    product_name: str
    quantity: int
    unit_price: float

class OrderCreate(BaseModel):
    customer_name: str
    items: list[OrderItem]
    notes: str | None = None

@app.post("/orders")
def create_order(order: OrderCreate):
    total = sum(item.quantity * item.unit_price for item in order.items)
    return {
        "customer": order.customer_name,
        "item_count": len(order.items),
        "total": round(total, 2),
    }

# POST /orders
# Body:
# {
#   "customer_name": "Bob",
#   "items": [
#     {"product_name": "Widget", "quantity": 3, "unit_price": 9.99},
#     {"product_name": "Gadget", "quantity": 1, "unit_price": 24.99}
#   ]
# }

items: list[OrderItem] expects a JSON array of order item objects. Each item in the array is validated against the OrderItem model. You can iterate over order.items to compute totals or transform data.

Deeply Nested and Reusable Models

from pydantic import BaseModel
from datetime import datetime

# Reusable models
class Timestamp(BaseModel):
    created_at: datetime
    updated_at: datetime | None = None

class Tag(BaseModel):
    name: str
    color: str = "gray"

class Author(BaseModel):
    name: str
    email: str

class Comment(BaseModel):
    author: Author
    body: str
    timestamp: Timestamp

# Composed model
class BlogPost(BaseModel):
    title: str
    body: str
    author: Author
    tags: list[Tag] = []
    comments: list[Comment] = []
    metadata: Timestamp

# This model reuses Author, Tag, Comment, and Timestamp
# The JSON Schema is generated automatically for the full structure
# Swagger UI shows all nested models with expandable sections

# Example usage:
post = BlogPost(
    title="FastAPI Guide",
    body="FastAPI is great...",
    author=Author(name="Alice", email="alice@example.com"),
    tags=[Tag(name="python"), Tag(name="api", color="blue")],
    metadata=Timestamp(created_at=datetime.now()),
)

Small, focused models are composed into complex structures. Author is reused in both BlogPost and Comment. Timestamp is reused wherever dates are needed. This DRY approach keeps models maintainable and consistent.

Key points

Concepts covered

Nested Models, List of Models, Complex Schemas, Reusable Models, Composition