FastAPI のエンドポイントに「辞書を返す正常系」と「JSONResponse を返すエラー系」の両方を持たせたくて、戻り値の型ヒントを dict | JSONResponse にした——その瞬間、アプリが起動すらしなくなりました。原因は型ヒントに Response 系のクラスを混ぜたことです。
結論(先に解決策)
そのエンドポイントのデコレータに response_model=None を付けるだけで直ります。
# NG: 戻り値 Union に JSONResponse を含めると起動時に落ちる
@router.get("/basic")
async def get_basic(...) -> dict[str, Any] | JSONResponse:
...
# OK: 応答モデルの自動生成を止める
@router.get("/basic", response_model=None)
async def get_basic(...) -> dict[str, Any] | JSONResponse:
...
FastAPI は戻り値の型ヒントから「レスポンスモデル」を自動で組み立てようとしますが、JSONResponse のような Response 派生クラスは Pydantic のフィールドにできません。response_model=None でその自動生成を明示的に無効化します。
症状
サーバー起動時(importの解決時)に、エンドポイントを呼ぶ前から例外で落ちます。
fastapi.exceptions.FastAPIError: Invalid args for response field!
Hint: check that dict[str, Any] | starlette.responses.JSONResponse
is a valid Pydantic field type
再現条件はシンプルで、戻り値の型ヒント(Union)に Response / JSONResponse などの応答クラスを含めているときです。
from fastapi import APIRouter
from fastapi.responses import JSONResponse
router = APIRouter()
@router.get("/basic")
async def get_basic() -> dict[str, Any] | JSONResponse: # ← これが原因
if error:
return JSONResponse(status_code=400, content={"status": "error"})
return {"status": "ok", "data": [...]}
原因
FastAPI は、戻り値の型ヒントを使って レスポンスのスキーマ(response_model)を自動生成します。生成の実体は Pydantic のフィールド構築です。
-
dict[str, Any]は Pydantic フィールドにできる - しかし
JSONResponse(= Starlette のResponse派生)は Pydantic のフィールド型として無効
Union の中に1つでも「Pydantic 化できない型」が混じると、フィールド全体の構築に失敗し、Invalid args for response field! を投げます。エラー文に Union の中身がそのまま出るので、... | starlette.responses.JSONResponse の部分が犯人だと分かります。
解決
応答モデルの自動生成を止めれば、型ヒントは「人間向けの注釈」として残しつつ FastAPI の検証対象から外れます。
@router.get("/basic", response_model=None)
async def get_basic() -> dict[str, Any] | JSONResponse:
if error:
return JSONResponse(status_code=400, content={"status": "error", "message": "..."})
return {"status": "ok", "data": [...]}
これで起動も通り、正常系は辞書、エラー系は JSONResponse という分岐もそのまま使えます。
代替案:そもそも JSONResponse を戻り値に混ぜない
「正常系・エラー系で共通の辞書形を返す」設計なら、JSONResponse を返さずに HTTPException を投げる方がきれいです。
from fastapi import HTTPException
@router.get("/basic")
async def get_basic() -> dict[str, Any]:
if error:
raise HTTPException(status_code=400, detail="Invalid area_code")
return {"status": "ok", "data": [...]}
この形なら戻り値は dict 単独になり、response_model=None も不要です。ステータスコードや本文を細かく自前制御したい場合のみ JSONResponse + response_model=None を選ぶ、という棲み分けが分かりやすいです。
まとめ
-
Invalid args for response field!は 戻り値の型ヒントを Pydantic 化できないときに出る - Union に
Response/JSONResponseを混ぜたのが典型例 - 即効薬は
@router.get(..., response_model=None) - 設計を見直せるなら
HTTPExceptionでエラーを投げて戻り値をdict単独にする
なお、この症状は e-Stat(政府統計)の人口データを FastAPI と PHP の二重実装で公開する API を作る途中で踏みました。正常時もエラー時も同じ {"status": ..., "data": ..., "meta": ...} 形に揃えるレスポンス設計の話は、別途まとめる予定です。