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
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
BaseModelsubclass 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:
- Define your leaf models first — the smallest building blocks, like
CustomerorItem. These have simple field types. - Compose larger models from them — create
Orderwith fields that referenceCustomerandlist[Item]. - Add validation constraints — use Pydantic's field types like
EmailStr,PositiveInt, or custom validators at the right level. - Use the model in FastAPI — declare it as a parameter type in a route, and FastAPI parses the incoming JSON against it.
- 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
dictmight be simpler, but you lose safety. - For very deep or recursive structures (like trees), look into Pydantic's
RecursiveModelsupport (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-validatorwhen usingEmailStr, leading to import errors at runtime. - Using generic
dictfor 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 (useField(default_factory=list)instead). - Nesting too deep for the sake of it — keep models mindful and aligned with actual data use.
Variations
- Use Pydantic's
RecursiveModelormodel_validatorfor self-referencing structures like trees. - Leverage
model_dump()withexclude_unset=Trueto partially serialize nested models for PATCH endpoints. - Type aliases with
Annotatedcan 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
BaseModelfor robust software. - You now have the foundation to move on to advanced Pydantic features in the next lesson.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.