Nested Data Models in FastAPI

Master nested data models in FastAPI. This tutorial explains how to structure complex Pydantic models, handle embedded objects and lists, and validate them effectively. Includes hands-on exercises, troubleshooting tips, and what to learn next.

Focus: working with nested data models

Sponsored

Your API is growing. Users aren't just posting simple strings anymore — they're sending entire JSON payloads with nested objects, arrays, and deeply structured data. If you've tried to model this kind of data with a flat Pydantic class, you know the pain: validation chaos, unreadable code, and endpoints that feel like they're one step away from breaking. In this lesson, you'll learn working with nested data models in FastAPI — the clean, declarative way to handle complex JSON structures with confidence and minimal code.

The problem this lesson solves

Picture this: your e-commerce API needs to accept an order that contains a customer, a list of items, and a shipping address. A naive approach might flatten everything into one giant model:

class Order(BaseModel):
    customer_name: str
    customer_email: str
    customer_address_street: str
    customer_address_city: str
    items_0_sku: str
    items_0_quantity: int
    items_1_sku: str
    # ... you get the idea

This is a nightmare. It's unreadable, brittle, and completely detached from the JSON structure your frontend team actually sends. Any change to the customer object ripples through your model, and nested lists become impossible to handle dynamically. This is exactly the problem nested data models solve — by letting you mirror your JSON structure directly in Python, with all of Pydantic's validation power applied recursively.

Before we dive in, here's what you'll walk away with:

  • You'll be able to explain the core idea behind nested data models — how they map one-to-one with your JSON payloads.
  • You'll complete a practical exercise building a multi-level API endpoint from scratch.
  • You'll connect this to the next lesson in the track, where we'll explore advanced validation and serialization.

Core concept / mental model

Think of Pydantic models as blueprints for JSON objects. A nested data model is simply a blueprint that contains other blueprints. Just like you can assemble a house from smaller components (walls, roof, windows), you assemble a complex API schema from smaller, focused models.

Here's the mental model:

  • Every BaseModel subclass represents one JSON object.
  • Fields can be other models — that maps to a nested { } in JSON.
  • Fields can be list[Model] — that maps to an array of objects [ { }, { } ].
  • FastAPI and Pydantic handle validation recursively — each level is checked automatically when data arrives.

For a concrete example, imagine an Order containing a Customer and a list of Item objects. In code, that looks like:

from pydantic import BaseModel

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

class Item(BaseModel):
    sku: str
    quantity: int

class Order(BaseModel):
    customer: Customer
    items: list[Item]

When a request comes in with a JSON body, Pydantic parses it, validates each nested object, and gives you an Order instance with strongly typed nested attributes. You can access things like order.customer.email and order.items[0].quantity — no manual dictionary drilling required.

How it works step by step

Let's break down the process, from request to validated object:

  1. Define your leaf models first — the smallest building blocks, like Customer or Item. These have simple field types.
  2. Compose larger models from them — create Order with fields that reference Customer and list[Item].
  3. Add validation constraints — use Pydantic's field types like EmailStr, PositiveInt, or custom validators at the right level.
  4. Use the model in FastAPI — declare it as a parameter type in a route, and FastAPI parses the incoming JSON against it.
  5. Access and process the data naturally — because you have typed objects, you get IDE autocomplete and runtime safety.

The beauty is that validation is recursive. If the JSON has a missing field inside a nested object, Pydantic raises a clear, precise error pointing to that exact location — not a generic "invalid payload" message.

Hands-on walkthrough

Let's build a real example: an API for creating orders with customers and items. Here's a complete, runnable FastAPI app:

# app.py
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr, PositiveInt

app = FastAPI()

class Customer(BaseModel):
    name: str
    email: EmailStr

class Item(BaseModel):
    sku: str
    quantity: PositiveInt

class Order(BaseModel):
    customer: Customer
    items: list[Item]
    notes: str | None = None

@app.post("/orders")
async def create_order(order: Order):
    total_items = sum(item.quantity for item in order.items)
    return {
        "message": f"Order for {order.customer.name} with {total_items} items received"
    }

Run it with uvicorn app:app --reload, then send a request with curl:

curl -X POST "http://localhost:8000/orders" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": {"name": "Alice", "email": "alice@example.com"},
    "items": [
      {"sku": "ABC123", "quantity": 2},
      {"sku": "XYZ789", "quantity": 1}
    ]
  }'

You'll get back:

{"message":"Order for Alice with 3 items received"}

If you send invalid data — say, an invalid email or a negative quantity — FastAPI returns a 422 response with detailed validation errors, pointing exactly to the problematic field.

Adding nested defaults and optionality

Sometimes you want a nested object to have defaults. You can do that directly:

class Address(BaseModel):
    street: str
    city: str
    country: str = "US"

class Customer(BaseModel):
    name: str
    email: EmailStr
    address: Address | None = None

Now address is optional, but if present, it must be a valid Address. This gives you flexible APIs without sacrificing type safety.

What about deeper nesting?

There's no limit — you can nest as deep as you need:

class Product(BaseModel):
    sku: str
    dimensions: dict[str, float]  # simple dict

class Order(BaseModel):
    customer: Customer
    items: list[Item]
    shipping: Address

Pydantic handles arbitrary depth, so your code stays clean even for complex structures.

Compare options / when to choose what

When deciding how to model nested data, you have a few options. Here's a comparison:

Approach Pros Cons Best for
Nested Pydantic models Strong type safety, validation, IDE support Requires upfront modeling Most APIs with structured data
Plain dict fields Quick to write, flexible No validation, error-prone Simple pass-through payloads
json.loads() + manual parsing Explicit control Verbose, repetitive, brittle Edge cases with irregular data

When to choose nested models:

  • You want automatic validation and documentation (FastAPI auto-generates OpenAPI schemas).
  • You need to enforce contracts between frontend and backend.
  • You plan to reuse sub-models across multiple endpoints.

When to consider alternatives:

  • For truly dynamic or unstructured data, a dict might be simpler, but you lose safety.
  • For very deep or recursive structures (like trees), look into Pydantic's RecursiveModel support (variations section).

Pro tip: Your API's documentation is only as good as your models. Nested models give you beautiful, interactive Swagger UI with dropdowns and type hints — a huge win for developer experience.

Troubleshooting & edge cases

Even experienced developers trip up on nested models. Here are common issues and how to fix them.

Error: missing for nested fields

If a nested object is required but absent, you'll get a validation error like field required under the parent field. Fix: make the nested field optional (Address | None = None) or ensure the client sends it.

Error: value is not a valid email address

This happens if you forget to add EmailStr properly. Remember to install email-validator (pip install email-validator) or use plain str for lenient validation.

Mistake: Using non-Pydantic types

If you use list instead of list[Item], Pydantic treats it as a generic list of dicts, and you lose type safety. Always annotate with the specific model.

Edge case: Performance with large nested payloads

Pydantic's validation has overhead. For very large requests, consider limiting nesting depth or optimizing your model structure.

Edge case: Optional fields vs. defaults

A field with = None is optional; a field with a default value like country = "US" is also optional but gets a default. Make sure you use the right one for your semantics.

What you learned & what's next

You've now mastered working with nested data models in FastAPI. You can:

  • Explain how nested models map to JSON structure.
  • Build complex, validated APIs with sub-models and lists.
  • Handle optionality and defaults gracefully.
  • Diagnose common pitfalls.

This is a solid foundation for production APIs. In the next lesson, we'll explore advanced Pydantic features like custom validators, model_validator, and serialization with model_dump() — skills that turn good APIs into great ones. You'll be ready.

Now, go model something real.

Practice recap

Try extending the order example: add a discount field to Item (float, between 0 and 1) with a validator, and make notes required. Use the interactive docs at /docs to test your new validation rules — then experiment with sending invalid payloads to see how errors become detailed.

Common mistakes

  • Forgetting to install email-validator when using EmailStr, leading to import errors at runtime.
  • Using generic dict for nested objects, losing all validation and making code harder to maintain.
  • Making nested objects required when they should be optional, causing API clients to get unexpected 422 errors.
  • Defining models with mutable defaults like = [], which can create shared state across requests (use Field(default_factory=list) instead).
  • Nesting too deep for the sake of it — keep models mindful and aligned with actual data use.

Variations

  1. Use Pydantic's RecursiveModel or model_validator for self-referencing structures like trees.
  2. Leverage model_dump() with exclude_unset=True to partially serialize nested models for PATCH endpoints.
  3. Type aliases with Annotated can project reusable schemas into multiple models without duplication.

Real-world use cases

  • Modeling e-commerce orders with customer, items, and shipping address for a checkout API.
  • Building a user profile API that accepts nested contact info and tags as embedded objects.
  • Designing a product catalog where each product has a nested spec object and a list of variants.

Key takeaways

  • Nested Pydantic models mirror your JSON structure, giving you type safety and validation across all levels.
  • Define leaf models first, then compose them into larger aggregates for clean, maintainable schema design.
  • FastAPI automatically handles recursive validation and generates rich OpenAPI docs from nested models.
  • Use optional fields and defaults wisely to keep endpoints flexible without losing safety.
  • Avoid generic dicts; every nested object deserves its own BaseModel for robust software.
  • You now have the foundation to move on to advanced Pydantic features in the next lesson.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.