Contents
1. FastAPI のリクエスト処理フローと Pydantic バリデーションの位置付け
FastAPI が受信した HTTP リクエストは、以下の順序で段階的に処理されます。この流れを把握すれば、どこでバリデーションが走り、エラーハンドリングが行われるかが一目瞭然です。
- ルーティング – パスと HTTP メソッドの一致判定。
- 依存性注入(Depends) – ミドルウェアや共通ロジックを実行。
- パラメータ抽出 –
Query,Path,Header,Cookieなどから生文字列を取得。 - Pydantic バリデーション – 抽出した値を型変換し、制約チェックを実施。
- エンドポイント関数実行 – 検証済みの Python オブジェクトが引数として渡される。
1.1 各ステップの詳細
| ステップ | 主な役割 | FastAPI が内部で呼び出すコンポーネント |
|---|---|---|
| ルーティング | URL とメソッドをマッピングし、対象関数を決定 | Starlette の Router |
| 依存性注入(Depends) | 前処理・認可・共通ロジックを実行 | fastapi.dependencies.Depends |
| パラメータ抽出 | 関数シグネチャの型ヒントからデータ取得 | fastapi.params.* ヘルパー |
| Pydantic バリデーション | 型変換+制約チェック、失敗時は ValidationError を送出 |
pydantic.BaseModel, Field |
| エンドポイント実行 | ビジネスロジックを実行し、レスポンス生成 | ユーザー定義関数 |
ポイント:バリデーションが失敗した瞬間に FastAPI は例外 (
pydantic.ValidationError) を捕捉し、標準的な 422 Unprocessable Entity レスポンスを自動生成します。
2. Pydantic v2 の主要機能と FastAPI 0.110 以降の対応状況
Pydantic がバージョン 2 に進化したことで、型ヒントとの統合がさらに深化しました。本節では新機能を中心に、FastAPI 側でどのようにサポートされているかを解説します。
2.1 Annotated とメタデータ統合
Python 標準の typing.Annotated を利用すると、型情報とバリデーション用メタデータを同一行で記述できます。FastAPI はこの構文を解析し、OpenAPI スキーマへ自動的に反映します。
|
1 2 3 4 5 6 7 8 9 |
from typing import Annotated from pydantic import BaseModel, Field class Product(BaseModel): price: Annotated[ float, Field(gt=0, description="正の価格(円)", example=1200) ] |
- メリット
- 型ヒントと制約が視覚的に結びつく。
Fieldの情報がそのまま OpenAPI に流れるので、ドキュメントが常に最新。
2.2 default_factory による安全なデフォルト値
ミュータブルオブジェクト(リスト・辞書)をフィールドのデフォルトとして設定したいときは、default_factory が推奨されます。Pydantic v2 でも同様にサポートされており、FastAPI のシリアライズ処理にも影響しません。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
from datetime import datetime from typing import List, Annotated from pydantic import BaseModel, Field class Order(BaseModel): id: int = Field(..., description="注文 ID") items: List[str] = Field(default_factory=list, description="商品コードの一覧") created_at: Annotated[ datetime, Field(description="作成日時", example="2024-01-15T09:30:00Z") ] = datetime.utcnow() |
2.3 FastAPI 0.110 の対応ポイント
| 機能 | FastAPI が提供するサポート |
|---|---|
Annotated |
自動的に OpenAPI スキーマへ反映、リクエストバリデーションで利用可能 |
default_factory |
デフォルト生成ロジックがそのままエンドポイント引数に適用 |
| バリデーション高速化(内部 core_schema) | Pydantic v2 の最適化に合わせて、シリアライズ/デシリアライズのオーバーヘッドを削減 |
結論:Pydantic v2 の新機能は FastAPI 0.110 とシームレスに統合され、コード可読性とドキュメント自動生成が大幅に向上します。
3. 型ヒントラッパー vs Pydantic モデル:実務での使い分け
FastAPI が提供する Query, Path, Body 系ヘルパーは「簡潔さ」が最大の利点です。一方、Pydantic モデルをそのままリクエストボディに使用すると 相関バリデーション や 再利用性 が格段に向上します。ここではそれぞれの特徴と典型的なユースケースを比較します。
3.1 Query / Path ラッパーの基本例
|
1 2 3 4 5 6 7 8 9 10 11 |
from fastapi import FastAPI, Query, Path app = FastAPI() @app.get("/users/{user_id}") def get_user( user_id: int = Path(..., gt=0, description="対象ユーザー ID"), limit: int = Query(20, ge=1, le=100, description="取得件数") ): return {"user_id": user_id, "limit": limit} |
- 適用シーン
- 単一または少数のパラメータで完結する検索・フィルタ系エンドポイント。
- パラメータごとのバリデーションがシンプルな場合。
3.2 Pydantic モデルを直接使用した例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
from fastapi import FastAPI, Body from pydantic import BaseModel, Field class SearchRequest(BaseModel): query: str = Field(..., min_length=3, description="検索キーワード") page: int = Field(1, ge=1, description="ページ番号") size: int = Field(20, ge=10, le=100, description="1 ページあたりの件数") app = FastAPI() @app.post("/search") def search(request: SearchRequest = Body(...)): return {"q": request.query, "page": request.page, "size": request.size} |
- 適用シーン
- 複数フィールド間で相関チェック(例:
page * size <= total_items)が必要な場合。 - 同一リクエストスキーマを複数エンドポイントで共有したいとき。
3.3 比較表
| 観点 | Query / Path ラッパー |
Pydantic モデル |
|---|---|---|
| 記述の簡潔さ | 高(1 行) | 中(クラス定義が必要) |
| 相関バリデーション | 難しい(個別 validator が必要) | 容易(@model_validator / root_validator) |
| 再利用性 | 低(エンドポイント限定) | 高(他箇所でもインポート可能) |
| OpenAPI スキーマ自動生成 | 標準的に対応 | カスタムスキーマが自由に設定可能 |
結論:シンプルなクエリは
Query/Pathで十分です。複雑構造やビジネスロジックが絡む場合は Pydantic モデルを直接使い、保守性とテスト容易性を高めましょう。
4. カスタムバリデーションの実装方法と依存性注入(Depends)の活用
4.1 @validator と @model_validator の基本パターン
@validator はフィールド単位、@model_validator(Pydantic v2) はモデル全体での検証に利用します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
from pydantic import BaseModel, validator, model_validator class Registration(BaseModel): email: str password: str confirm_password: str @validator("email") def email_domain(cls, v): if not v.endswith("@example.com"): raise ValueError("example.com ドメインのメールが必要です") return v @model_validator(mode="after") def passwords_match(cls, values): pw, cpw = values.get("password"), values.get("confirm_password") if pw != cpw: raise ValueError("パスワードと確認用パスワードが一致しません") return values |
- ポイント
- フィールドレベルは
@validator、相関チェックは@model_validator(mode="after")が推奨。 - バリデーションエラーは自動的に
ValidationErrorとして集約され、FastAPI は 422 に変換します。
4.2 Depends を用いた共通検証ロジック
Depends は依存性注入だけでなく、エンドポイント横断的なバリデーション関数としても活用できます。以下はメールドメインをチェックする例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from fastapi import Depends, FastAPI, Query, HTTPException, status def validate_email_domain(email: str = Query(...)): if not email.endswith("@example.com"): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="example.com ドメインのメールが必要です", ) return email app = FastAPI() @app.get("/users") def list_users(valid_email: str = Depends(validate_email_domain)): # ここに来ると既に検証済み return {"message": f"Valid email {valid_email}"} |
- 利点
- 複数エンドポイントで同一ロジックを再利用可能。
- 外部サービス呼び出し(例:認可サーバ)と組み合わせた高度な検証が実装しやすい。
注意:
Depends内でHTTPExceptionを投げると、FastAPI はそのままレスポンスへ変換します。Pydantic の自動 422 とは別にハンドリングしたい場合は、この方法を選びます。
4.3 カスタム例外ハンドラで統一フォーマット化
独自エラー構造(error_code, user_message)が必要なときは、以下のように例外ハンドラを追加します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
from fastapi import Request, HTTPException from fastapi.responses import JSONResponse import pydantic @app.exception_handler(pydantic.ValidationError) async def validation_exception_handler(request: Request, exc: pydantic.ValidationError): return JSONResponse( status_code=422, content={ "error_code": "VALIDATION_ERROR", "details": exc.errors(), }, ) |
- 効果
- すべての Pydantic バリデーションエラーが一貫した JSON 形式で返る。
- フロントエンド側のエラーハンドリングが簡素化。
5. エラーハンドリング・パフォーマンスベンチマーク・実務ベストプラクティス
5.1 自動生成される 422 レスポンスとカスタムハンドリングの選択肢
| 手法 | 実装コスト | カスタマイズ性 | 主な利用シーン |
|---|---|---|---|
自動 422(Pydantic が投げた ValidationError) |
低 | 低(JSON の構造は固定) | 小規模 API、内部向けサービス |
| 例外ハンドラでラップ | 中 | 高(エラーコード・メッセージの追加が可能) | 外部クライアントへ統一フォーマットで提供したい場合 |
Depends 内で HTTPException |
低~中 | 中(ステータスや detail を自由に設定) | エンドポイント単位で個別エラーメッセージを出すケース |
5.2 パフォーマンス比較(公式ベンチマーク情報)
FastAPI のリポジトリに含まれるベンチマークスクリプトと、2024 年 10 月に公開された公式レポートによると、Pydantic v2 に移行した環境ではシリアライズ速度が約20%向上し、メモリ使用量も約15%削減されました。数値は以下のテスト構成で測定されています。
| テスト項目 | Pydantic v1 (FastAPI 0.95) | Pydantic v2 (FastAPI 0.110) |
|---|---|---|
| JSON シリアライズ(1,000 件) | 1.34 ms/req | 1.07 ms/req |
| バリデーション処理時間(平均) | 5.2 ms/req | 4.1 ms/req |
| メモリ使用量(ピーク) | 12 MiB | 10 MiB |
出典:FastAPI 公式ベンチマーク (
fastapi/tests/performance) – https://github.com/tiangolo/fastapi/tree/master/tests/performance
5.3 実務でのベストプラクティス
- バリデーションはモデル側に集中
- フィールド制約は
Field/Annotated、相関チェックは@model_validatorに実装。 - 共通ロジックは Depends で抽象化
- 認可や外部 API 呼び出しを伴う検証は依存性注入にまとめ、テスト容易性を確保。
- エラーレスポンスは統一フォーマット
- プロジェクト全体で
validation_exception_handlerを設定し、フロントが期待する JSON 構造を提供。 - Pydantic v2 への段階的移行
- まず
Annotatedとdefault_factoryの利用から始め、既存モデルはBaseModel継承のままで動作確認。 - OpenAPI ドキュメントを定期的に検証
httpxやswagger-cliで生成されたスキーマが期待通りか CI に組み込み、破壊的変更を防止。
6. 実務チェックリスト & まとめ
| チェック項目 | 推奨設定・実装例 |
|---|---|
| リクエストフローの可視化 | uvicorn --log-level debug でルーティングと依存性注入を確認 |
| Pydantic v2 の利用 | price: Annotated[float, Field(gt=0)]、default_factory を適所で使用 |
| 型ヒントラッパーの選択基準 | クエリは Query/Path、複雑構造はモデルを直接使用 |
| 共通バリデーション | def validate_x(...): ... を Depends で注入 |
| 例外ハンドラの統一 | @app.exception_handler(pydantic.ValidationError) を実装 |
| ベンチマーク測定 | wrk -t12 -c400 -d30s http://localhost:8000/items/1 で応答時間を取得 |
| ドキュメント自動生成 | openapi.json が最新か CI で比較 |
最後に
- FastAPI のリクエスト処理は 「ルーティング → Depends → Pydantic バリデーション → エンドポイント」 のシンプルなフローです。
- Pydantic v2 の
Annotatedとdefault_factoryはコードの可読性と安全性を大幅に向上させ、FastAPI 0.110 が完全サポートしています。 - シンプルなクエリは FastAPI ヘルパー、複雑構造や相関チェックが必要な場合は Pydantic モデル を直接使い分けると保守性が上がります。
- カスタム検証は
@validator/@model_validatorとDependsの組み合わせで実装し、例外ハンドラでエラー形式を統一すれば、クライアント側の実装負荷も低減できます。
公式リファレンス(https://fastapi.tiangolo.com/)と FastAPI GitHub リポジトリにあるサンプルコードを手元に置き、実際に uvicorn で起動してエンドポイントごとのバリデーション挙動 を確認しながら導入すると、スムーズに移行できるでしょう。