Browse Source

perf: optimize jsonable_encoder by short-circuiting Pydantic model serialization

pull/15965/head
Your Name 2 weeks ago
parent
commit
7742bf883e
  1. 60
      benchmark_encoder.py
  2. 17
      fastapi/encoders.py

60
benchmark_encoder.py

@ -0,0 +1,60 @@
import time
from typing import List, Optional
from pydantic import BaseModel
from fastapi.encoders import jsonable_encoder
class SubModel(BaseModel):
name: str
value: int = 42
class MainModel(BaseModel):
id: int
title: str
sub: SubModel
items: List[SubModel]
maybe: Optional[str] = None
def run_benchmark():
sub = SubModel(name="test")
model = MainModel(
id=1,
title="hello",
sub=sub,
items=[sub] * 50, # 50 items to have a decent dictionary size
)
iterations = 20000
print(f"Benchmarking jsonable_encoder over {iterations} iterations...")
# 1. Optimized Path (Direct return)
# Warmup
for _ in range(100):
jsonable_encoder(model)
start_time = time.perf_counter()
for _ in range(iterations):
jsonable_encoder(model)
optimized_time = time.perf_counter() - start_time
# 2. Original Path (Double serialization via model_dump + recursive dict encoding)
# Warmup
for _ in range(100):
jsonable_encoder(model.model_dump(mode="json"))
start_time = time.perf_counter()
for _ in range(iterations):
# We simulate the exact old logic: model_dump(mode="json") followed by recursive jsonable_encoder
obj_dict = model.model_dump(mode="json")
jsonable_encoder(obj_dict)
original_time = time.perf_counter() - start_time
print(f"Original Code Path: {original_time:.4f} seconds")
print(f"Optimized Code Path: {optimized_time:.4f} seconds")
print(f"Speedup: {original_time / optimized_time:.2f}x")
if __name__ == "__main__":
run_benchmark()

17
fastapi/encoders.py

@ -241,6 +241,23 @@ def jsonable_encoder(
if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude) # type: ignore[assignment] # ty: ignore[invalid-assignment]
if isinstance(obj, BaseModel):
if not exclude_none and not exclude_defaults:
obj_dict = obj.model_dump(
mode="json",
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_none=exclude_none,
exclude_defaults=exclude_defaults,
)
if not sqlalchemy_safe:
return obj_dict
return {
k: v for k, v in obj_dict.items()
if not (isinstance(k, str) and k.startswith("_sa"))
}
# Fallback to recursive call for recursive exclude_none/exclude_defaults cleanup
obj_dict = obj.model_dump(
mode="json",
include=include,

Loading…
Cancel
Save