サイトアイコン IT & ライフハックブログ|学びと実践のためのアイデア集

FastAPIのResponseValidationErrorとは?原因の見分け方と正しい直し方

programming codes screengrab

Photo by Myburgh Roux on Pexels.com

FastAPIでレスポンスモデルを指定すると、返却データはクライアントへ送られる前に検証されます。
ResponseValidationErrorは、そのデータが宣言した型や構造に合わないときに発生する例外です。

このエラーは、クライアントの入力を検証するときのRequestValidationErrorとは性質が異なります。
レスポンスの不整合はAPI側の実装上の問題なので、サーバーのログを確認し、レスポンスモデルと実際の返却値を比較します。

ResponseValidationErrorが発生する仕組み

レスポンスモデルは、APIが返すデータの契約です。
FastAPIは関数の戻り値をPydanticモデルで定義したレスポンスに照らして検証し、問題がなければ必要なフィールドをシリアライズして返します。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserResponse(BaseModel):
    id: int
    name: str

@app.get('/user', response_model=UserResponse)
async def get_user():
    return {'id': 1}

この例ではnameが必須なのに、戻り値には含まれていません。
FastAPIは不完全な成功レスポンスをそのまま返さず、レスポンス検証エラーとして扱います。

通常はサーバーエラーになるため、クライアント側のレスポンスだけでは原因のフィールドまで分からない場合があります。
ログに出るlocmsgtypeを手掛かりに、どの場所で何が拒否されたかを確認します。

ログで確認する主な項目
項目 内容
loc 問題があるフィールドや配列内の位置
msg 必須項目の不足や型の不一致などの理由
type エラーを分類する識別子

表記はFastAPIとPydanticのバージョンによって異なることがあります。
固定の文言を探すのではなく、場所と理由を読み取ります。

主な原因と修正方法

必須フィールドが不足している

前の例では、モデルが要求するnameを返すように修正します。

@app.get('/user', response_model=UserResponse)
async def get_user():
    return {'id': 1, 'name': 'Alice'}

ただし、存在しない値を仮の文字列で埋めるのは適切ではありません。
データ取得処理の欠落なのか、モデル側で省略を認めるべき項目なのかを先に判断します。

Noneを許可していない

name: strNoneを返すと、モデルの契約を満たしません。
未設定を正式な状態として扱う場合だけ、モデルにもその条件を表します。

class UserResponse(BaseModel):
    id: int
    name: str | None = None

Pydantic v2ではstr | Noneと書くだけではフィールド自体が必須のままです。
省略も認めるなら、上の例のようにデフォルト値を指定します。

「フィールドがない状態」と「フィールドはあり、値がnullの状態」は同じではありません。
APIの利用側が両者を区別するなら、どちらを返すかを仕様で決めます。

型へ変換できない値を返している

次のidは整数として解釈できないため、レスポンス検証に失敗します。

@app.get('/user', response_model=UserResponse)
async def get_user():
    return {'id': 'not-a-number', 'name': 'Alice'}

文字列の'1'は、Pydanticの通常の検証では整数へ変換されることがあります。
型不一致を調べるときはPython上の型だけで判断せず、実際の値とモデルの検証設定を確認します。

ネストしたデータや配列の一部が合わない

入れ子のオブジェクトや配列では、内側の一要素だけが原因になることがあります。
locをたどり、親オブジェクト、配列の位置、フィールド名の順に返却データを確認します。

データベースの列名とレスポンスのフィールド名が異なる場合は、取得結果をそのまま返さず、レスポンスモデルへ明示的に詰め替えると不整合を見つけやすくなります。

誤解しやすい設定

response_model_exclude_unset

response_model_exclude_unset=Trueは、検証を通ったデータから、明示的に設定されていないデフォルト項目を出力しないための指定です。
必須フィールドが欠けた戻り値を有効にする指定ではありません。

class UserResponse(BaseModel):
    id: int
    name: str | None = None

@app.get('/user', response_model=UserResponse, response_model_exclude_unset=True)
async def get_user():
    return {'id': 1}

この例ではnameにデフォルト値があるため、戻り値はモデルを満たします。
そのうえで、未設定のnameが出力から除外されます。

extraの設定

Pydanticのextra設定は、モデルに定義していない余分なフィールドをどう扱うかを決めるものです。
extra='allow'にしても、必須フィールドの不足やNone、変換できない値は解決しません。

原因を切り分ける手順

  1. スタックトレースから失敗したエンドポイントを特定する。
  2. locから問題のフィールドや配列の位置を特定する。
  3. モデルの必須項目、許可するNone、型を確認する。
  4. 返却直前のデータを、機密情報を除いて開発環境のログで確認する。
  5. データ生成処理を直すか、仕様上妥当な場合だけモデルを変更する。

外部サービスやデータベースの結果を返す場合は、境界でレスポンスモデルへ変換すると原因を局所化できます。

@app.get('/user', response_model=UserResponse)
async def get_user():
    raw_user = {'id': 1, 'name': 'Alice'}
    return UserResponse.model_validate(raw_user)

再発を防ぐテスト

ステータスコードだけでなく、JSONの構造と値もテストします。
必須項目が欠ける経路やNoneになる経路を含めると、レスポンスモデル変更時の回帰を検出しやすくなります。

def test_get_user_response_shape(client):
    response = client.get('/user')

    assert response.status_code == 200
    assert response.json() == {'id': 1, 'name': 'Alice'}

よくある疑問

422エラーとの違いは何ですか

リクエストのパス、クエリ、本文などが入力モデルを満たさない場合は、クライアント入力の検証エラーとして扱われます。
ResponseValidationErrorは、エンドポイントが作った出力がレスポンスモデルを満たさない問題です。

response_modelを外せば直りますか

検証を外せば例外が見えなくなる場合はありますが、返却データの不整合は残ります。
レスポンスの検証と出力フィールドの制御も失われるため、モデルと戻り値のどちらが誤っているかを直します。

確認するポイント

ResponseValidationErrorは、宣言したレスポンスと実際のデータのずれを知らせます。
設定で例外だけを消さず、ログが示す位置からデータの生成元までたどり、仕様に合う戻り値またはモデルへ修正するのが基本です。

モバイルバージョンを終了