StackTraces
FastAPI已驗證

FastAPI 422 Unprocessable Entity 排查與修復

@debug_master32 天前更新閱讀 6 min00

一句話先說結論:422 Unprocessable Entity 不是鑑權、不是接口沒寫,而是 FastAPI 用 Pydantic 校驗你的請求,發現數據跟模型定義對不上。最快的排查就是讀響應體 detail 陣列裡的 loc(錯在哪)、msg(為什麼錯)、type(錯誤類型)、input(實際收到的值)。修復大多落在三件事上:別用 ORM 模型直接收請求、字段名/類型對齊、Optional 字段給預設值。

背景

FastAPI 的一個核心賣點就是“用類型註解自動做校驗”:路由函數參數標註成 Pydantic 模型後,框架在處理請求前先跑一遍校驗,能強轉的強轉(字符串 "5" 轉 int、ISO 日期轉 datetime),不能過的直接甩 422,根本輪不到你的業務程式碼執行。

這個 422 區別於 400(JSON 根本不是合法 JSON)、415(Content-Type 不對)、500(程式碼崩了)——它含義明確:請求到達了、格式也懂,但“內容”不符合契約。

現象

請求失敗,響應體長這樣:

{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "character_id"],
      "msg": "Field required",
      "input": { "user_message": "hello" }
    },
    {
      "type": "int_parsing",
      "loc": ["body", "age"],
      "msg": "Input should be a valid integer",
      "input": "abc"
    }
  ]
}

四個字段就是排查的全部線索:

字段 含義
loc 出錯位置,["body","字段名"] 是請求體、["query","x"] 是查詢參數、["path","x"] 是路徑參數
msg 人類可讀的描述,如 Field required、Input should be a valid integer
type 機器可讀的錯誤類型,如 missing、int_parsing、string_type
input 實際收到的值

根因分析

422 的高頻來源,基本能歸成這幾類:

  1. 必填字段缺失:模型字段沒預設值,客戶端沒傳。
  2. 類型不匹配:該是 int 傳了字符串,該是物件傳了列表。
  3. 字段名對不上:最常見是前後端大小寫規範不一致——前端 userId,後端 user_id。
  4. Content-Type 不對:該發 JSON 卻發了表單,或反之;FastAPI 按 header 決定怎麼解析 body。
  5. 嵌套層級錯位:模型要 {"profile": {"age": 25}},客戶端卻平鋪成了 {"age": 25}。
  6. 拿 ORM 模型直接收請求:把 Conversation(帶 id、timestamp 這類不該由客戶端填的字段)當請求體模型,天然容易觸發**「沒傳 id 就缺字段」**,或者反過來放行本不該暴露的字段。

解決方案

方案 A:用專用輸入模型(DTO),別用 ORM 模型收請求

這是最該改的結構性問題。定義只含客戶端該傳字段的輸入模型:

from pydantic import BaseModel

class CreateConversationRequest(BaseModel):
    character_id: int
    user_message: str

路由用它接收,再手動構造 ORM 物件:

@router.post("/chat")
def chat(data: CreateConversationRequest, session: Session = Depends(get_session)):
    conversation = Conversation(
        user_id=current_user_id,
        character_id=data.character_id,
        user_message=data.user_message,
    )
    session.add(conversation)
    session.commit()
    return {"success": True}

這樣 id、user_id、timestamp 這些數據庫側字段不會被客戶端塞進來,契約清晰。

方案 B:Optional 字段給預設值

不確定一定有的字段,標 Optional 並給預設:

from typing import Optional

class UserCreate(BaseModel):
    name: str                      # 必填
    email: str                     # 必填
    age: Optional[int] = None      # 可空
    bio: str = ""                  # 可选,默认空串

方案 C:字段名用別名對齊前端

前端 camelCase、後端 snake_case,用 Field(alias=...) 在“對外 JSON key”和“內部變數名”之間搭橋:

from pydantic import BaseModel, Field

class Item(BaseModel):
    character_id: int = Field(alias="characterId")

    model_config = {"populate_by_name": True}  # 允许两种名字都收

方案 D:控制未知字段策略

extra 決定遇到模型裡沒定義的字段怎麼辦:

from pydantic import ConfigDict

class StrictModel(BaseModel):
    model_config = ConfigDict(extra="forbid")   # 多余字段直接报错(推荐生产)

class LooseModel(BaseModel):
    model_config = ConfigDict(extra="ignore")   # 忽略多余字段

排查階段臨時用 extra="allow",能幫你一眼看出客戶端到底多發/漏發了哪些字段。

一個升級坑:Pydantic v1 → v2

FastAPI 0.100 起切到了 Pydantic v2,錯誤格式和寫法都變了,升級時容易踩:

  • 錯誤 type 碼變了(v1 的 value_error.missing / type_error.integer 變成 v2 的 missing / int_parsing)。
  • class Config 改為 model_config = ConfigDict(...)。
  • @validator/@root_validator 改為 @field_validator / @model_validator。
  • parse_obj() 改為 model_validate(),parse_raw() 改為 model_validate_json()。

如果自定義了 422 錯誤處理器,注意 exc.errors() 在 v2 裡返回的是 dict 列表,不是 v1 的 tuple 列表。

小結

  • 422 的第一動作永遠是讀 detail 的 loc + msg + type,別瞎猜。
  • 結構性建議:request 用獨立輸入模型、response 用輸出模型,把 ORM 模型和 API 契約隔開。
  • 字段名對齊(alias)、可選字段給預設、extra 策略,這三招覆蓋大部分場景。
  • 升級 Pydantic v2 時,把校驗器和 config 寫法一起遷過去,順手檢查自定義 422 handler。

References(參考來源)

最後更新於 2026-09-09

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法