Pydantic Model Serialization Performance in FastAPI
Key takeaways:
- Profile a representative payload first so you optimize the real bottleneck.
- Use
model_dump_jsonfor single-pass JSON output. - Return a trimmed response model so you serialize only needed fields.
- Reuse a
TypeAdapterfor non-model shapes instead of rebuilding it. - Avoid re-validation by passing trusted models through or using
model_construct.
This guide is the serialization-focused companion to Performance Optimization for Models and complements Handling Deeply Nested JSON Models Efficiently.
The Problem This Solves
Serialization quietly becomes a hot-path cost as payloads grow, especially for nested responses returned at high request rates. The good news is that the biggest wins are simple and measurable: emit JSON in one pass, serialize fewer fields, and stop repeating validation.
Prerequisites
- Pydantic v2 (the Rust core enables single-pass serialization).
- A representative payload and a way to time it.
Step-by-Step Implementation
1. Profile first
import time
def profile_serialize(model, n: int = 1000) -> float:
start = time.perf_counter()
for _ in range(n):
model.model_dump_json()
return (time.perf_counter() - start) / n * 1000 # ms per call
2. Single-pass JSON
# Efficient: one pass on the Rust core.
body = order.model_dump_json()
# Avoid on large graphs: builds a dict, then re-encodes in Python.
# body = json.dumps(order.model_dump())
3. Trim with a response model
from pydantic import BaseModel
class OrderListItem(BaseModel):
id: int
total: int # The list view needs two fields, not the whole order.
@router.get("/orders", response_model=list[OrderListItem])
async def orders() -> list[OrderListItem]:
return await fetch_order_list()
4. Reuse a TypeAdapter and skip re-validation
from pydantic import TypeAdapter
# Built once at module scope; reused for every serialization of this shape.
ORDERS = TypeAdapter(list[OrderListItem])
def dump_orders(items: list[OrderListItem]) -> bytes:
return ORDERS.dump_json(items)
Edge Cases and Gotchas
- Computed fields. A
computed_fieldruns on every serialization; cache its input if it is expensive. by_aliaseverywhere. Aliasing is cheap, but inconsistent alias conventions across nested models produce confusing output; standardize.- Premature optimization. Without profiling you may optimize a path that is not the bottleneck; measure first.
Verification
def test_serialization_within_budget(big_order):
# Guardrail on a representative payload; tune to your SLA.
assert profile_serialize(big_order, n=200) < 5.0 # ms per call
Related Reading
- Up to the topic: Performance Optimization for Models.
- Related guides: Handling Deeply Nested JSON Models Efficiently and Caching Strategies.