Pydantic v2 中 BaseModel 必须用 model_validate 替代 parse_obj,且 FastAPI 中需用 Body(embed=True) 处理嵌套 JSON;Field(default_factory=...) 可区分“未提供”与“传 null”;ValidationError.errors() 提供结构化错误信息。

Pydantic v2 的 BaseModel 必须用 model_validate 替代 parse_obj
很多老项目迁移到 Pydantic v2 后,接口校验突然抛出 AttributeError: type object 'MyModel' has no attribute 'parse_obj'。这是因为 v2 彻底移除了 parse_obj、parse_raw 等旧方法,统一用 model_validate(校验 Python 字典)和 model_validate_json(校验 JSON 字符串)。
实际写 FastAPI 接口时,你通常不需要手动调用这些——FastAPI 内部已自动使用 model_validate 处理请求体。但如果你在中间件、工具函数或单元测试中手动实例化模型,就得立刻改掉:
from pydantic import BaseModel
<p>class UserCreate(BaseModel):
name: str
age: int</p><h1>✅ 正确(v2)</h1><p>user = UserCreate.model_validate({"name": "Alice", "age": 30})</p><h1>❌ 错误(v1 风格,v2 报错)</h1><h1>user = UserCreate.parse_obj({"name": "Alice", "age": 30})- 别依赖 IDE 自动补全的旧方法名,查官方文档确认当前版本 API
-
model_validate默认 strict=False,允许类型强制转换(如把字符串"25"转成int),如需严格模式,加参数strict=True - 若输入是 bytes 或 str 格式的 JSON,必须用
model_validate_json,否则会误把 JSON 字符串当普通字段值处理
FastAPI 中 Body 参数未设 embed=True 导致 422 错误却不报具体字段
当你定义一个嵌套 Body 模型,比如 POST /users 接收 {"user": {"name": "Bob", "age": 28}},但 Pydantic 模型直接写成 UserCreate,FastAPI 默认会期望顶层 JSON 就是模型字段——即 {"name": "Bob", "age": 28}。如果实际传了带外层 key 的结构,就会返回模糊的 422,且错误信息里只显示 value_error.missing,不提是哪个字段缺失。
立即学习“Python免费学习笔记(深入)”;
根本原因是 FastAPI 没把外层 user 当作“包装键”,而是当成多余字段忽略,导致内层字段全丢失。
- 解决方法:显式用
Body(embed=True)告诉 FastAPI 把整个 body 包进一个 key 里校验 - 示例:
from fastapi import Body from pydantic import BaseModel <p>class UserCreate(BaseModel): name: str age: int</p><p>@app.post("/users") def create_user(user: UserCreate = Body(..., embed=True)): return user - 此时客户端必须传
{"user": {"name": "Bob", "age": 28}};若想兼容两种格式,得用两个不同 endpoint 或自定义依赖项
用 Field(default=...) 和 Field(default_factory=...) 区分“空值”与“未提供”
API 常见需求:某些字段可选,但需要知道用户是“没传这个字段”,还是“传了 null/empty 字符串”。比如更新接口中,PUT /users/123 只改名字,不希望年龄被意外清零。
Pydantic 的 Field(default=None) 无法区分“用户传了 "age": null” 和 “压根没传 age 字段”。真正可控的方式是用 default_factory + 自定义标记对象:
from pydantic import BaseModel, Field
from typing import Any
<p>_unset = object() # 唯一哨兵值</p><p>class UserUpdate(BaseModel):
name: str | None = None
age: int | None = Field(default_factory=lambda: _unset)</p><pre class='brush:python;toolbar:false;'>def get_updated_fields(self) -> dict[str, Any]:
return {k: v for k, v in self.__dict__.items() if v is not _unset}</pre>-
default_factory在每次实例化时调用,确保每次都是新哨兵,避免可变默认值陷阱 -
Field(default=None)表示“允许为 None”,但不会帮你判断是否用户主动提交了 None - 若用
Optional[int]+Field(default=None),用户传{"age": null}和不传age都会导致age == None,无法区分
自定义错误信息要用 ValidationError 的 errors() 而非 str(e)
很多人在异常处理器里直接 str(e) 返回给前端,结果得到一长串带文件路径、行号、内部类名的 traceback 式文本,既难读又暴露服务细节。Pydantic 的 ValidationError 提供了结构化的 errors() 方法,返回标准字典列表。
FastAPI 默认已做了这层封装,但如果你在非 FastAPI 场景(如 CLI 工具、后台任务)手动校验,就得自己处理:
try:
user = UserCreate.model_validate(data)
except ValidationError as e:
# ✅ 结构化输出,前端可直接解析
errors = [
{"field": ".".join(err["loc"]), "msg": err["msg"], "type": err["type"]}
for err in e.errors()
]
raise HTTPException(422, detail=errors)-
e.errors()每个元素含loc(字段路径,如["age"]或["items", 0, "price"])、msg、type - 不要用
json.loads(str(e))或正则提取——str(e)格式不稳定,v2 之后还可能变化 - 若需国际化错误文案,应在
Field(..., description="年龄必须大于 0")之外,用自定义 validator +raise ValueError("年龄不能为负"),再统一捕获转译
最易被忽略的一点:Pydantic 的字段校验发生在模型实例化**之前**,所以即使你写了 @field_validator("age"),只要输入连基本类型都过不了关(比如传字符串 "abc" 给 int 字段),自定义 validator 根本不会执行——错误直接卡在类型转换阶段。要覆盖这类底层转换逻辑,得用 @field_serializer 或自定义 __pydantic_core_schema__,代价很高,通常应优先约束前端传参规范。


















