始めに
FastAPIはデフォルトではJSONResponse
を使用してレスポンスします。FastAPI公式ヘルプにはパフォーマンスを向上させるためにORJSONResponse
の使用を提案するページがあります。
fastapi[all]
にorjson
が含まれていることもあり、ORJSONResponse
を使用して動作確認をしてみたのですが、個人的な使用目的としては早くならなかったのでメモとして残しておくことにします。
※ ベンチマークの取り方がおかしい、等があったら教えていただきたいです。
Pydantic v2: 3.2411秒 (1000回のリクエスト) ORJSONResponse: 3.3306秒 (1000回のリクエスト)
環境
- Python
- 3.13
- FastAPI
- 0.115.12
- Pydantic
- 2.11.2
- orjson
- 3.10.16
実装
基本的にはFastAPIのチュートリアルどおりにpydantic
を使用してレスポンスモデルをマッピングします。この時のシリアライズ方法を変更する方法で試していました。検証パターンは載せていますが、どちらで実施しても傾向は変わらなかったので、実際にブログに乗せているのは1つだけです。
パターン1
FastAPI全体にJSONResponse
ではなく、ORJSONResponse
を使用する。
from fastapi import FastAPI from fastapi.responses import ORJSONResponse app = FastAPI(default_response_class=ORJSONResponse)
パターン2
継承元にorjson.dumpsを使用してシリアライズさせる設定を追加する。
import orjson from pydantic import BaseModel, ConfigDict class BaseModelB(BaseModel): model_config = ConfigDict( json_serializer=orjson.dumps )
動作検証
次のコードでpydantic
だけを使用したコードとpydantic
とorjson
を使用したコードを比較していました。
ある程度階層が深い方がシリアライズの差異がわかりやすいかと思い、JSON構造で第三階層までマッピングするようなコードにしています。
import orjson from fastapi import APIRouter from fastapi.responses import ORJSONResponse from pydantic import BaseModel, ConfigDict router = APIRouter() class BaseModelA(BaseModel): pass class BaseModelB(BaseModel): model_config = ConfigDict( json_serializer=orjson.dumps ) # GitHubユーザAPIの深い構造を再現 class Repository(BaseModelA): id: int name: str full_name: str private: bool html_url: str description: str class Organization(BaseModelA): login: str id: int url: str repos_url: str class UserResponse(BaseModelA): login: str id: int node_id: str avatar_url: str gravatar_id: str url: str html_url: str followers_url: str following_url: str gists_url: str starred_url: str subscriptions_url: str organizations_url: str repos_url: str events_url: str received_events_url: str type: str site_admin: bool name: str company: str blog: str location: str email: str hireable: bool bio: str twitter_username: str public_repos: int public_gists: int followers: int following: int created_at: str updated_at: str repositories: list[Repository] organizations: list[Organization] metadata: dict[str, str] class RepositoryB(BaseModelB): id: int name: str full_name: str private: bool html_url: str description: str class OrganizationB(BaseModelB): login: str id: int url: str repos_url: str class UserResponseB(BaseModelB): login: str id: int node_id: str avatar_url: str gravatar_id: str url: str html_url: str followers_url: str following_url: str gists_url: str starred_url: str subscriptions_url: str organizations_url: str repos_url: str events_url: str received_events_url: str type: str site_admin: bool name: str company: str blog: str location: str email: str hireable: bool bio: str twitter_username: str public_repos: int public_gists: int followers: int following: int created_at: str updated_at: str repositories: list[RepositoryB] organizations: list[OrganizationB] metadata: dict[str, str] def generate_deep_response(): return UserResponse( login="octocat", id=1, node_id="MDQ6VXNlcjE=", avatar_url="https://github.com/images/error/octocat_happy.gif", gravatar_id="", url="https://api.github.com/users/octocat", html_url="https://github.com/octocat", followers_url="https://api.github.com/users/octocat/followers", following_url="https://api.github.com/users/octocat/following{/other_user}", gists_url="https://api.github.com/users/octocat/gists{/gist_id}", starred_url="https://api.github.com/users/octocat/starred{/owner}{/repo}", subscriptions_url="https://api.github.com/users/octocat/subscriptions", organizations_url="https://api.github.com/users/octocat/orgs", repos_url="https://api.github.com/users/octocat/repos", events_url="https://api.github.com/users/octocat/events{/privacy}", received_events_url="https://api.github.com/users/octocat/received_events", type="User", site_admin=False, name="monalisa octocat", company="GitHub", blog="https://github.com/blog", location="San Francisco", email="octocat@github.com", hireable=False, bio="There once was...", twitter_username="monatheoctocat", public_repos=2, public_gists=1, followers=20, following=0, created_at="2008-01-14T04:33:35Z", updated_at="2008-01-14T04:33:35Z", repositories=[ Repository( id=1300192, name="Spoon-Knife", full_name="octocat/Spoon-Knife", private=False, html_url="https://github.com/octocat/Spoon-Knife", description="Test repository" ) ], organizations=[ Organization( login="github", id=1, url="https://api.github.com/orgs/github", repos_url="https://api.github.com/orgs/github/repos" ) ], metadata={ "rate_limit": "1000", "remaining": "990", "api_version": "2022-11-28" } ).model_dump() @router.get("/pydantic_only", response_model=UserResponse) async def pydantic_only(): return UserResponse(**generate_deep_response()) @router.get("/pydantic_with_orjson", response_model=UserResponseB) async def with_orjson(): return UserResponseB(**generate_deep_response())
テスト自体は1000回実施した結果を見ています。
@pytest.mark.skip class TestSpeedPydantic: def test_pydantic_only_response(self): result = timeit.timeit(lambda: client.get("/pydantic/pydantic_only"), number=1000) print(f"Pydantic v2: {result:.4f}秒 (1000回のリクエスト)") def test_pydantic_with_orjson_response(self): result = timeit.timeit(lambda: client.get("/pydantic/pydantic_with_orjson"), number=1000) print(f"ORJSONResponse: {result:.4f}秒 (1000回のリクエスト)") Pydantic v2: 3.2411秒 (1000回のリクエスト) ORJSONResponse: 3.3306秒 (1000回のリクエスト)
パターン1, パターン2も含めて何回か施行したのですが、結果的に言えばPydantic V2
を使用している場合にはorjson
で処理するほうが余計に遅くなる結果が出ました。
完全にvalidationを行わないでORJSONResponse
に直接マッピングさせても処理速度は遅くなりました。
ソースコード
参考情報
- https://fastapi.tiangolo.com/advanced/custom-response
- https://github.com/pydantic/pydantic/discussions/6388
終わりに
正直、この検証方法が正しいかどうかの自信はないです。Pydantic V1のころだったらorjson
を使用することによって、優位な結果が出たかもしれませんが、V2では遅くなる結果が出ました。
意図的にfastapi[all]
を使用しているメリットが正直自分には見当たらないので、依存関係をfastapi
だけにして依存ライブラリを絞ったほうがメリットが多そうです。
この検証方法が誤っている、またはfastapi[all]
での有用なライブラリを知っている方はコメントくれると嬉しいです。