🐱 うさねこ教室 Python と可観測性の教室

バリデーションとエラー設計

🐱 この章の目次

Pydantic による入力バリデーション

FastAPI は Pydantic モデルを通じて入力値を自動バリデーションします。 型が合わない場合やフィールド制約に違反した場合、422 Unprocessable Entity が返ります。 カスタムバリデータを @field_validator で定義すれば、ビジネスルールもスキーマ層で表現できます。

from pydantic import BaseModel, field_validator


class OrderCreate(BaseModel):
    quantity: int
    price: float

    @field_validator("quantity")
    @classmethod
    def quantity_must_be_positive(cls, v: int) -> int:
        if v <= 0:
            raise ValueError("quantity は 1 以上にしてください")
        return v

RFC 7807 Problem Details

RFC 7807(Problem Details for HTTP APIs) は、エラーレスポンスの構造を標準化する仕様です。 typetitlestatusdetailinstance のフィールドでエラー情報を表現します。 統一されたフォーマットにより、クライアント側のエラーハンドリングを一貫して実装できます。

{
  "type": "https://example.com/errors/validation",
  "title": "Validation Error",
  "status": 422,
  "detail": "quantity は 1 以上にしてください",
  "instance": "/orders",
  "errors": [
    {
      "field": "quantity",
      "message": "quantity は 1 以上にしてください"
    }
  ]
}

カスタム例外ハンドラ

FastAPI では @app.exception_handler で例外クラスごとにレスポンスをカスタマイズできます。 アプリケーション全体で RFC 7807 形式のエラーを返すには、共通ハンドラを定義します。

from fastapi import Request
from fastapi.responses import JSONResponse


class AppError(Exception):
    def __init__(self, status: int, detail: str, error_type: str):
        self.status = status
        self.detail = detail
        self.error_type = error_type


@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
    return JSONResponse(
        status_code=exc.status,
        content={
            "type": exc.error_type,
            "title": "Application Error",
            "status": exc.status,
            "detail": exc.detail,
            "instance": str(request.url.path),
        },
    )

バリデーションエラーのカスタマイズ

FastAPI デフォルトの 422 レスポンスは独自形式ですが、RequestValidationError のハンドラを上書きすることで RFC 7807 形式に統一できます。 エラーの国際化やフィールド名のマッピングもこのハンドラ内で行います。

from fastapi.exceptions import RequestValidationError


@app.exception_handler(RequestValidationError)
async def validation_error_handler(request: Request, exc: RequestValidationError):
    errors = [
        {"field": ".".join(str(l) for l in e["loc"][1:]), "message": e["msg"]}
        for e in exc.errors()
    ]
    return JSONResponse(
        status_code=422,
        content={
            "type": "https://example.com/errors/validation",
            "title": "Validation Error",
            "status": 422,
            "detail": "入力値に問題があります",
            "instance": str(request.url.path),
            "errors": errors,
        },
    )