Pydantic v2 rewrote validation on Rust (pydantic-core), but seniors still burn CPU by forcing Python round-trips on every field. The unfair advantage is staying inside the typed core path: TypeAdapter, model_validate with the right mode, and validators that do not allocate throwaway dicts on the hot edge.
⚡ TL;DR: Prefer
TypeAdapter[T].validate_python/validate_jsonover ad-hocmodel_validate+.model_dump()ping-pong; useValidationInfosparingly; mark pure field checks asmode='after'only when you must; avoid@model_validator(mode='wrap')on every request. Pair with orjson and msgspec and Bytes vs str Boundaries.
Stay on the core path
At high QPS the tax is copies: JSON → dict → model → dict → JSON. Collapse that.
# ✅ FastAPI edge: validate JSON bytes once, dump once
from pydantic import BaseModel, TypeAdapter, Field, ConfigDict
class CreateOrder(BaseModel):
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
sku: str = Field(min_length=1, max_length=64)
qty: int = Field(ge=1, le=10_000)
currency: str = Field(pattern=r"^[A-Z]{3}$")
adapter = TypeAdapter(CreateOrder)
def parse_order(raw: bytes) -> CreateOrder:
# ✅ validate_json keeps work in pydantic-core; no intermediate dict
return adapter.validate_json(raw)
def to_wire(order: CreateOrder) -> bytes:
# ✅ serialize from model without Python-side field looping
return adapter.dump_json(order, by_alias=True)
# ❌ Classic tax: decode → dict → validate → dump → encode
import json
from pydantic import TypeAdapter
# body = CreateOrder.model_validate(json.loads(raw)) # Python dict alloc
# return json.dumps(body.model_dump()).encode() # second copy storm
Validators that do not thrash
field_validator and model_validator drop you into Python. Use them for invariants you cannot express in Field constraints—not for trivial coercions.
from pydantic import field_validator, model_validator
class Money(BaseModel):
amount_cents: int = Field(ge=0)
currency: str
@field_validator("currency")
@classmethod
def upper_currency(cls, v: str) -> str:
# ✅ cheap, pure, no I/O
if len(v) != 3:
raise ValueError("currency must be ISO-4217 length 3")
return v.upper()
@model_validator(mode="after")
def reject_zero_usd_sentinel(self) -> "Money":
# ✅ rare business rule — keep off the millions-QPS path if possible
if self.currency == "USD" and self.amount_cents == 0:
raise ValueError("zero USD amounts require explicit free-sku path")
return self
| Pattern | Cost | When OK |
|---|---|---|
Field(ge=…, pattern=…) |
Core | Always prefer |
field_validator pure |
Python hop | Coercion / normalize |
model_validator(mode='wrap') |
Highest | Cross-field with raw access only |
Nested model_validate in loops |
Disaster | Pre-build TypeAdapter |
FastAPI wiring without double parse
Starlette already buffered the body. Do not parse JSON twice.
from fastapi import FastAPI, Request, Response
app = FastAPI()
order_adapter = TypeAdapter(CreateOrder)
@app.post("/orders")
async def create_order(request: Request) -> Response:
raw = await request.body()
order = order_adapter.validate_json(raw) # ✅
# persist...
return Response(
content=order_adapter.dump_json(order),
media_type="application/json",
)
❌ Declaring order: CreateOrder and also await request.json() in the same handler — you paid validation twice.
Closing checklist
✅ Dos
– ✅ Use TypeAdapter.validate_json on raw bytes at the edge
– ✅ Prefer Field constraints over Python validators
– ✅ Reuse module-level adapters (construction is not free)
– ✅ Set extra='forbid' on public write models
– ✅ Benchmark allocs with tracemalloc / py-spy under load
❌ Don’ts
– ❌ Don’t model_dump() then re-validate the dict in middleware
– ❌ Don’t put DB/IO inside validators
– ❌ Don’t use mode='wrap' as a default habit
– ❌ Don’t enable expensive validate_assignment on hot mutable models
Related reading
- orjson and msgspec: Serialization Wins for FastAPI Worker Fleets
- Bytes vs str Boundaries: Cut Encode Tax in Python Network Services
- Python tracemalloc in Workers
- Incremental pyright in CI
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
