FastAPI Basics • Lesson 9

Body - Nested Models

Master complex nested data structures with FastAPI and Pydantic. Learn to use lists, sets, nested models, and deeply nested structures for powerful APIs.

🎯 What You'll Learn

  • Create models with list and set fields using proper type annotations
  • Build nested models by embedding one Pydantic model inside another
  • Handle lists of nested models for complex data structures
  • Use Dict fields for key-value mappings with type safety
  • Understand deeply nested models and their validation benefits

Body - Nested Models

🎯 What You'll Learn

This lesson covers the complete official FastAPI "Body - Nested Models" tutorial. You'll master creating arbitrarily deeply nested models using FastAPI and Pydantic, enabling you to handle complex data structures with full validation and documentation.

By the end of this lesson, you'll understand how to:

  • Create models with list and set fields using proper type annotations
  • Build nested models by embedding one Pydantic model inside another
  • Handle lists of nested models for complex data structures
  • Use Dict fields for key-value mappings with type safety
  • Understand deeply nested models and their validation benefits

📚 1. List Fields

You can define an attribute to be a Python list:

from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: list = []  # Basic list without type specification

This makes tags a list, but doesn't specify the type of elements inside the list.

🎯 2. List Fields with Type Parameters

Python has a specific way to declare lists with internal types using "type parameters":

Import typing's List

For Python versions before 3.9, import List from the typing module:

from typing import List, Union
from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: List[str] = []  # List of strings

Modern Python (3.9+)

In Python 3.9 and above, you can use the standard list:

class Item(BaseModel):
    name: str
    tags: list[str] = []  # Modern syntax

Request body example:

{
    "name": "Laptop",
    "description": "Gaming laptop",
    "price": 999.99,
    "tags": ["electronics", "computers", "gaming"]
}

🔄 3. Set Fields

You can use Set types for collections of unique items:

from typing import Set, Union
from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()  # Set of unique strings

Benefits of Sets:

  • Automatically removes duplicates
  • Validates uniqueness
  • More efficient for membership testing

Request body example:

{
    "name": "Laptop",
    "price": 999.99,
    "tags": ["electronics", "computers", "electronics"]
}

Result: tags will be {"electronics", "computers"} (duplicate removed)

🏗️ 4. Nested Models

You can define a Pydantic model as the type of an attribute:

from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

class Image(BaseModel):
    url: HttpUrl  # Special Pydantic type for URLs
    name: str

class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()
    image: Union[Image, None] = None  # Nested model

Request body example:

{
    "name": "Laptop",
    "price": 999.99,
    "tags": ["electronics", "computers"],
    "image": {
        "url": "https://example.com/laptop.jpg",
        "name": "Laptop Image"
    }
}

Special Pydantic Types

  • HttpUrl: Validates that the string is a valid HTTP URL
  • EmailStr: Validates email addresses (requires email-validator)
  • UUID: Validates UUID strings
  • datetime: Handles datetime objects

📋 5. Lists of Nested Models

You can have lists containing nested models:

class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()
    images: List[Image] = []  # List of nested models

Request body example:

{
    "name": "Gaming Setup",
    "price": 1999.99,
    "images": [
        {
            "url": "https://example.com/setup1.jpg",
            "name": "Main Setup"
        },
        {
            "url": "https://example.com/setup2.jpg", 
            "name": "Side View"
        }
    ]
}

🗂️ 6. Deeply Nested Models

You can create arbitrarily deep nesting:

class Image(BaseModel):
    url: HttpUrl
    name: str

class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()
    images: List[Image] = []

class Offer(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    items: List[Item]  # Nested list of items with their own nested images

🗃️ 7. Dict Fields

You can use Dict for key-value mappings:

from typing import Dict
from fastapi import FastAPI

app = FastAPI()

@app.post("/index-weights/")
async def create_index_weights(weights: Dict[int, float]):
    return weights

Request body example:

{
    "1": 0.5,
    "2": 1.0,
    "3": 0.75
}

Response:

{
    "1": 0.5,
    "2": 1.0,
    "3": 0.75
}

🌟 Key Concepts

Type Safety Benefits

Nested models provide:

  • Automatic validation at every level
  • Type conversion for compatible types
  • Clear error messages showing exactly which nested field failed
  • IDE support with autocomplete for nested attributes
  • Automatic documentation in OpenAPI schema

Validation Flow

  1. Top-level validation: Main model fields are validated
  2. Nested validation: Each nested model validates its own fields
  3. Collection validation: Lists/Sets validate each element
  4. Type conversion: Automatic conversion where possible
  5. Error aggregation: All validation errors collected and returned

Performance Considerations

  • Validation overhead: Deeper nesting = more validation work
  • Memory usage: Nested objects use more memory
  • Serialization cost: Complex structures take longer to serialize/deserialize
  • Network payload: Larger JSON payloads

💡 Best Practices

Model Organization

# Organize from simple to complex
class Address(BaseModel):
    street: str
    city: str
    country: str

class User(BaseModel):
    name: str
    email: EmailStr
    address: Address  # Simple nesting

class Order(BaseModel):
    id: int
    user: User        # Nested user
    items: List[Item] # List of nested items
    total: float

Validation Constraints

from pydantic import Field

class Item(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    tags: Set[str] = Field(max_items=10)  # Limit set size
    images: List[Image] = Field(max_items=5)  # Limit list size

Optional vs Required Nesting

class Item(BaseModel):
    name: str
    # Optional nested model
    image: Union[Image, None] = None
    # Required nested model
    category: Category
    # Optional list (can be empty)
    tags: List[str] = []

🎯 Complete Example

from typing import Dict, List, Set, Union
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl

app = FastAPI()

class Image(BaseModel):
    url: HttpUrl
    name: str

class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()
    image: Union[Image, None] = None

class ItemWithImages(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    images: List[Image] = []

@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    results = {"item_id": item_id, "item": item}
    return results

@app.put("/items/{item_id}/images")
async def update_item_with_images(item_id: int, item: ItemWithImages):
    results = {"item_id": item_id, "item": item}
    return results

@app.post("/index-weights/")
async def create_index_weights(weights: Dict[int, float]):
    return weights

🔗 What's Next?

In the next lesson, you'll learn about Extra Data Types, where you'll discover FastAPI's support for additional data types like UUID, datetime, timedelta, frozenset, bytes, and Decimal.

📖 Additional Resources

💡 Hint

Use List[str] for typed lists, Set[str] for unique collections, and embed Pydantic models inside other models for complex nested structures.

Ready to Practice?

Now that you understand the theory, let's put it into practice with hands-on coding!

Start Interactive Lesson