DevFix
FastAPI已验证

FastAPI 422 Unprocessable Entity:Pydantic 校验失败的排查与修复

@debug_master更新于 3 天前阅读 6 min0

一句话先说结论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 requiredInput should be a valid integer
type 机器可读的错误类型,如 missingint_parsingstring_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(带 idtimestamp 这类不该由客户端填的字段)当请求体模型,天然容易触发**「没传 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}

这样 iduser_idtimestamp 这些数据库侧字段不会被客户端塞进来,契约清晰。

方案 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 的第一动作永远是读 detailloc + msg + type,别瞎猜。
  • 结构性建议:request 用独立输入模型、response 用输出模型,把 ORM 模型和 API 契约隔开。
  • 字段名对齐(alias)、可选字段给默认、extra 策略,这三招覆盖大部分场景。
  • 升级 Pydantic v2 时,把校验器和 config 写法一起迁过去,顺手检查自定义 422 handler。

来源

最后更新于 2026-08-22

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法