Nested Model Serialization in FastAPI

Nested model serialization is how Pydantic turns a composed object graph — an order holding line items holding products — into JSON, and how you control which fields appear and what the traversal costs.

This topic lives in Advanced Pydantic Validation and Serialization. It is where validation meets output, and the traversal cost it introduces is a primary subject of Performance Optimization for Models.

A nested model graph serialized into JSON An Order model contains a list of LineItem models, each containing a Product model. Serialization walks the whole tree, so cost grows with total node count. Order LineItem LineItem Product Product
Serialization visits every node in the graph. Total node count — not top-level field count — drives the cost, which is why trimming nested data matters.

Core Mechanics: Composition and model_dump

Nested models are just models referenced as field types. Serialization with model_dump() produces a nested dict, and model_dump_json() produces a JSON string directly on the Rust core in a single pass. The single-pass form is the efficient default for responses.

from pydantic import BaseModel


class Product(BaseModel):
    id: int
    name: str


class LineItem(BaseModel):
    product: Product
    quantity: int


class Order(BaseModel):
    id: int
    items: list[LineItem]


# One pass to a JSON string — no intermediate Python dict to build and discard.
payload = order.model_dump_json()

Production Implementation: Selection, Aliases, and Computed Fields

Control the output contract explicitly. Aliases rename fields on the wire, exclude/include trim them, and computed_field adds derived values such as a total.

from pydantic import BaseModel, Field, computed_field


class Order(BaseModel):
    id: int = Field(serialization_alias="orderId")   # camelCase on the wire.
    items: list[LineItem]

    @computed_field   # Derived value included in output and schema, not stored input.
    @property
    def total_quantity(self) -> int:
        return sum(item.quantity for item in self.items)

The strategies for keeping deeply nested responses fast and memory-safe are detailed in Handling Deeply Nested JSON Models Efficiently.

Async and Performance Notes

Serialization is CPU-bound and runs on the event loop thread, so a very large response can block other requests briefly while it serializes. The remedies are to return only the fields clients need, to paginate large collections rather than serializing thousands of nested items at once, and to cache serialized output for hot, rarely-changing resources, which connects to Caching Strategies.

Testing Strategy

Assert the serialized shape, including aliases and computed fields, so the wire contract is pinned:

def test_order_serialization_contract():
    order = Order(id=1, items=[LineItem(product=Product(id=9, name="X"), quantity=3)])
    data = order.model_dump(by_alias=True)
    assert data["orderId"] == 1            # Alias applied.
    assert data["total_quantity"] == 3     # Computed field present.

Failure Modes and Debugging

  • Redundant re-validation. Re-parsing nested data through another model re-validates the whole tree; pass models directly or use model_construct for trusted data.
  • Accidental field leakage. Returning ORM-backed models can serialize internal columns; use explicit response models.
  • Unbounded collections. Serializing an unpaginated nested list can exhaust memory and stall the loop; paginate.
  • Alias mismatches. Validation aliases and serialization aliases differ; set both when the wire name must round-trip.