Pydantic v2 默认禁止额外字段且校验错误提示不友好,需配置 model_config={"extra": "allow"} 并扁平化 errors() 输出;自定义校验器必须显式 return 值,model_dump() 替代 dict() 且默认行为不同。
BaseModel 默认不再允许额外字段很多老项目升级到 Pydantic v2 后,接口接收 JSON 时突然报 ValidationError,提示 “extra fields not permitted”,其实不是数据错了,而是模型默认关掉了未知字段。这和 v1 的宽松行为完全不同。
解决方法是显式开启允许:
from pydantic import BaseModelclass UserRequest(BaseModel): name:
str age: int
class Config: extra = "allow" # v1 写法,v2 中已弃用v2 正确写法改用
model_config:class UserRequest(BaseModel): name: str age: intmodel_config = {"extra": "allow"}
"allow":忽略多余字段,不报错"ignore":静默丢弃(不推荐,容易掩盖前端传参错误)"forbid":严格禁止(默认值,也是最安全的选择)ValidationError 的 errors() 输出难读当请求体含多层嵌套(比如 address.city 缺失),默认的 e.errors() 返回的是带 loc 元组的字典,像 [{"loc": ["address", "city"], "msg": "Field required"}],直接返回给前端不友好。
建议封装一层扁平化处理:
def format_pydantic_errors(e):
return [
{"field": ".".join(err["loc"]), "message": err["msg"]}
for err in e.errors()
]
使用示例
try:
UserRequest(**data)
except ValidationError as e:
return {"errors": format_pydantic_errors(e)}
loc 给前端,尤其是内部字段名(如 __root__)Field(alias=...),loc 仍显示原始字段名,需手动映射别名ValidationError 会合并所有错误,errors() 仍可用@field_validator 做业务逻辑校验,但忘记 return value
自定义校验器里最常踩的坑:写了逻辑判断却没返回值,导致字段被清空为 None。Pydantic 不会帮你兜底。
比如校验手机号格式后忘了返回:
from pydantic import field_validator@field_validator("phone") def validate_phone(cls, v): if not re.match(r"^1[3-9]\d{9}$", v): raise ValueError("invalid phone number")
❌ 忘了 return v → 字段值变成 None
@field_validator 函数必须显式 return 处理后的值(哪怕没改)skip_on_failure=True,但慎用,容易掩盖必填逻辑model_dump() 和 dict() 在 v2 中行为不同v2 废弃了 .dict(),改用 .model_dump()。但很多人迁移时只改函数名,没注意默认参数变化:旧版 .dict(exclude_unset=True) 对应新版的 .model_dump(exclude_unset=True),但默认值变了。
关键差异:
.model_dump() 默认导出所有字段(含未设置的默认值).model_dump(exclude_unset=True) 才等价于旧版 .dict()
default_factory,且没被赋值,exclude_unset=True 也不会排除它——得用 exclude_defaults=True
API 响应常用组合:
user = UserResponse(name="Alice", age=25) user.model_dump(exclude_unset=True, exclude_none=True)
这样既不传未设置字段,也不传 None 值,更符合 RESTful 接口习惯。
字段级别控制还有 alias、by_alias、exclude 等,但多数场景只需记住:不加 exclude_unset 就很可能把默认值也吐出去,前端拿到意外字段。