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.
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.
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.
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.
Nested Models, List of Models, Complex Schemas, Reusable Models, Composition