Skip to main content

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:

python
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 model
  • Order contains a list of Item models in its items 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:

json
{
"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:

python
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 inside ContactInfo
  • ContactInfo is nested inside User
  • 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:

python
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:

python
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:

python
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 and HttpUrl for validation

Using Nested Models with Response Models

You can use different models for input and output, which is especially useful with nested structures:

python
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:

python
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:

  1. Create a nested model structure for an e-commerce system with products, categories, and reviews
  2. Implement a simple API for a document management system with folders containing files
  3. Build a model for a social network where users can have friends and posts with comments
  4. Create an API endpoint that accepts a complex nested structure and returns a simplified version of it

Additional Resources

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! :)