FastAPI Nested Models
When building APIs, you'll often need to work with complex data structures that contain nested objects. FastAPI, combined with Pydantic, makes handling these nested structures intuitive and type-safe. In this guide, we'll explore how to create and work with nested models in FastAPI applications.
Introduction to Nested Models
Nested models in FastAPI allow you to represent complex data structures where one model contains instances of other models. This reflects real-world relationships between data entities, such as:
- A user having multiple addresses
- An order containing multiple items
- A blog post with author details and comments
By using nested models, you can:
- Enforce data validation at all levels of your data structure
- Create clear documentation that reflects the relationships between your data
- Work with complex JSON payloads in a type-safe manner
Creating Basic Nested Models
Let's start with a simple example of nested models:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
app = FastAPI()
# Child model
class Item(BaseModel):
name: str
price: float
is_offer: bool = False
# Parent model containing the child model
class Order(BaseModel):
order_id: int
customer_name: str
items: List[Item] # A list of Item models
total_amount: float
# Example API endpoint using the nested model
@app.post("/orders/")
async def create_order(order: Order):
return {"order_created": order}
In this example:
Item
is a standalone modelOrder
contains a list ofItem
models in itsitems
attribute- FastAPI will automatically validate that each item in the list conforms to the
Item
model specifications
Example Input and Output
When making a POST request to /orders/
, you would provide JSON like:
{
"order_id": 1,
"customer_name": "John Doe",
"items": [
{
"name": "Keyboard",
"price": 59.99,
"is_offer": true
},
{
"name": "Mouse",
"price": 29.99
}
],
"total_amount": 89.98
}
The response would echo back this data, confirming the order was created with all nested items.
Multiple Levels of Nesting
You can create more complex structures with multiple levels of nesting:
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
from typing import List, Optional
app = FastAPI()
class Address(BaseModel):
street: str
city: str
country: str
postal_code: str
class ContactInfo(BaseModel):
email: EmailStr
phone: str
address: Address # Nested Address model
class User(BaseModel):
user_id: int
name: str
contact_info: ContactInfo # Nested ContactInfo model
is_active: bool = True
@app.post("/users/")
async def create_user(user: User):
return {"user_created": user}
In this example:
Address
is nested insideContactInfo
ContactInfo
is nested insideUser
- FastAPI will validate the entire structure, including all nested objects
Using Nested Models with Optional Fields
Sometimes you might want certain nested models to be optional:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional, List
app = FastAPI()
class Image(BaseModel):
url: str
name: str
class Product(BaseModel):
name: str
description: str
price: float
tax: Optional[float] = None
images: Optional[List[Image]] = None # Optional list of nested models
@app.post("/products/")
async def create_product(product: Product):
return product
With this setup, the images
field in a Product can be:
- A valid list of Image objects
None
- Omitted from the request entirely
Recursive Models
Sometimes you need to define models that reference themselves, like in hierarchical structures:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI()
class Comment(BaseModel):
content: str
author: str
# This would cause an error because Comment isn't fully defined yet
# replies: List[Comment]
# Instead, we need to use forward references:
class Comment(BaseModel):
content: str
author: str
replies: Optional[List["Comment"]] = []
class Config:
arbitrary_types_allowed = True
Comment.update_forward_refs()
@app.post("/comments/")
async def create_comment(comment: Comment):
return comment
The update_forward_refs()
method is crucial here - it tells Pydantic to update the model after it's been fully defined, resolving the forward reference.
Practical Example: Blog API with Nested Models
Let's create a more comprehensive example of a blog API with nested models:
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr, HttpUrl
from typing import List, Optional
from datetime import datetime
app = FastAPI()
class UserBase(BaseModel):
username: str
email: EmailStr
full_name: Optional[str] = None
bio: Optional[str] = None
class Tag(BaseModel):
name: str
class Image(BaseModel):
url: HttpUrl
caption: Optional[str] = None
class Comment(BaseModel):
content: str
author: UserBase
created_at: datetime
class Post(BaseModel):
title: str
content: str
author: UserBase
tags: List[Tag] = []
images: List[Image] = []
comments: List[Comment] = []
published: bool = False
created_at: datetime
updated_at: Optional[datetime] = None
@app.post("/posts/")
async def create_post(post: Post):
# In a real app, you'd save this to a database
return {"post_id": "123", **post.dict()}
@app.get("/posts/sample")
async def get_sample_post():
"""Return a sample post with nested data for demonstration"""
return {
"title": "Understanding FastAPI Nested Models",
"content": "This is a sample blog post...",
"author": {
"username": "fastapi_user",
"email": "[email protected]",
"full_name": "FastAPI Enthusiast"
},
"tags": [
{"name": "fastapi"},
{"name": "pydantic"}
],
"images": [
{
"url": "https://example.com/images/fastapi-logo.png",
"caption": "FastAPI Logo"
}
],
"comments": [
{
"content": "Great article!",
"author": {
"username": "commenter1",
"email": "[email protected]"
},
"created_at": "2023-01-01T12:00:00"
}
],
"published": True,
"created_at": "2023-01-01T10:00:00"
}
This example demonstrates:
- Multiple levels of nesting (Post contains Comments which contain Users)
- Optional nested fields
- Lists of nested models
- Using specialized Pydantic types like
EmailStr
andHttpUrl
for validation
Using Nested Models with Response Models
You can use different models for input and output, which is especially useful with nested structures:
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
from typing import List, Optional
app = FastAPI()
# Input models
class AddressCreate(BaseModel):
street: str
city: str
country: str
postal_code: str
class UserCreate(BaseModel):
name: str
email: EmailStr
password: str # We require password on input
addresses: List[AddressCreate] = []
# Output models (no password)
class Address(BaseModel):
id: int
street: str
city: str
country: str
postal_code: str
class User(BaseModel):
id: int
name: str
email: EmailStr
# No password field in the output
addresses: List[Address] = []
@app.post("/users/", response_model=User)
async def create_user(user: UserCreate):
# In a real app, you'd hash the password and save to DB
# Here we're simulating created objects with IDs
created_addresses = []
for i, addr in enumerate(user.addresses):
created_addresses.append({
"id": i + 1,
**addr.dict()
})
return {
"id": 1,
"name": user.name,
"email": user.email,
# Note: password is not included here
"addresses": created_addresses
}
This pattern allows you to:
- Accept different fields on input than what you expose on output
- Hide sensitive fields like passwords
- Add system-generated fields like IDs to your responses
Schemas with Nested Models and SQLAlchemy
In real applications, you'll often use nested models with ORM libraries like SQLAlchemy. Here's how you can structure your code:
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session, relationship
from pydantic import BaseModel
from typing import List
# Database setup (simplified)
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# SQLAlchemy models (database tables)
class UserDB(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
email = Column(String, unique=True, index=True)
# Relationship to addresses
addresses = relationship("AddressDB", back_populates="user")
class AddressDB(Base):
__tablename__ = "addresses"
id = Column(Integer, primary_key=True, index=True)
street = Column(String)
city = Column(String)
country = Column(String)
user_id = Column(Integer, ForeignKey("users.id"))
# Relationship to user
user = relationship("UserDB", back_populates="addresses")
# Pydantic models (API models)
class AddressBase(BaseModel):
street: str
city: str
country: str
class AddressCreate(AddressBase):
pass
class Address(AddressBase):
id: int
user_id: int
class Config:
orm_mode = True
class UserBase(BaseModel):
name: str
email: str
class UserCreate(UserBase):
addresses: List[AddressCreate] = []
class User(UserBase):
id: int
addresses: List[Address] = []
class Config:
orm_mode = True
# FastAPI app
app = FastAPI()
# Dependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/users/", response_model=User)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
# Create user in DB
db_user = UserDB(name=user.name, email=user.email)
db.add(db_user)
db.commit()
db.refresh(db_user)
# Create addresses
for addr in user.addresses:
db_address = AddressDB(**addr.dict(), user_id=db_user.id)
db.add(db_address)
db.commit()
db.refresh(db_user)
return db_user
This example demonstrates:
- SQLAlchemy models with relationships
- Pydantic models for API input and output
- Converting between SQLAlchemy and Pydantic models using
orm_mode = True
- Handling nested data when reading from and writing to a database
Summary
Nested Pydantic models in FastAPI provide a powerful way to handle complex data structures in your API:
- They let you define hierarchical relationships between your data entities
- All nested data is automatically validated
- You can use them with different levels of complexity (optional fields, lists, recursive structures)
- They integrate well with ORMs like SQLAlchemy
- They provide clear API documentation through OpenAPI/Swagger
When working with nested models, remember these best practices:
- Keep your models focused on a specific domain or entity
- Use different models for input and output when appropriate
- Use optional fields when parts of the structure might not always be present
- Consider the performance implications of deeply nested structures
- Update forward references when creating recursive models
Exercises
To reinforce your understanding:
- Create a nested model structure for an e-commerce system with products, categories, and reviews
- Implement a simple API for a document management system with folders containing files
- Build a model for a social network where users can have friends and posts with comments
- Create an API endpoint that accepts a complex nested structure and returns a simplified version of it
Additional Resources
- FastAPI documentation on Pydantic Models
- Pydantic documentation on nested models
- FastAPI documentation on response models
- Pydantic field types
- SQLAlchemy with FastAPI
Happy coding with FastAPI's nested models!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)