校验器
本教程共 50 篇 · 第 20 篇 · 更新于 2026-08-12 · 约 7 分钟阅读
本节目标:学会用 Pydantic v2 的 @field_validator 和 @model_validator,写出字段级和跨字段的自定义校验逻辑,并接到 FastAPI 请求体。
上一章我们用 Field() 做了长度、大小等现成约束。但有些规则它表达不了,比如「两次密码必须一致」「用户名只能含字母数字」。这种要自己写逻辑的场景,就用「校验器(validator)」。
20-1 字段级校验:@field_validator
@field_validator 装饰在一个类方法上,表示「这个字段在赋值前后,要经过我的检查」。默认 mode="after",也就是字段已经通过基础类型校验之后才跑你的逻辑。
from pydantic import BaseModel, field_validator
class User(BaseModel):
username: str
age: int
@field_validator("username")
@classmethod
def username_must_be_alnum(cls, v: str) -> str:
if not v.isalnum():
raise ValueError("用户名只能包含字母和数字")
return v
@field_validator("age")
@classmethod
def age_must_be_positive(cls, v: int) -> int:
if v <= 0:
raise ValueError("年龄必须大于 0")
return v
要点先记牢:
- 装饰器必须配
@classmethod。 - 方法第一个参数是
cls,第二个v是字段的值。 - 校验通过就
return v,让数据继续往下走。 - 不合法就
raise ValueError("原因"),Pydantic 会把它转成ValidationError。
Note在校验器里不要直接
raise ValidationError。Pydantic 约定你抛普通的ValueError或AssertionError,它会自动包成漂亮的校验错误。
20-2 before 与 after 两种模式
mode="after" 看到的是已转换好的值,比如字符串 "18" 已经变成整数 18。如果你想在「转换之前」就处理原始输入(比如清洗格式),就用 mode="before"。
from pydantic import BaseModel, field_validator
class Contact(BaseModel):
phone: str
@field_validator("phone", mode="before")
@classmethod
def clean_phone(cls, v: object) -> object:
if isinstance(v, str):
# 去掉所有非数字字符,比如括号和横线
digits = "".join(ch for ch in v if ch.isdigit())
return digits
return v
这里 v 还是原始字符串,可能是 "138-1234-5678"。我们把它清成纯数字再交还给 Pydantic。这种「先归一化再校验」的模式非常实用。
Tip经验法则:要改输入格式用
mode="before";要检查转换后的值用默认的mode="after"。
20-3 一个校验器管多个字段
@field_validator 可以一次接收多个字段名,对它们套用同一套逻辑。
from pydantic import BaseModel, field_validator
class Account(BaseModel):
password: str
confirm: str
@field_validator("password", "confirm")
@classmethod
def no_spaces(cls, v: str) -> str:
if " " in v:
raise ValueError("密码不能包含空格")
return v
这样 password 和 confirm 都会被检查。不过要注意,字段是按声明顺序逐个校验的,跨字段比对时优先考虑下面的 model_validator。
20-4 跨字段校验:@model_validator
当规则需要同时看多个字段(比如两次密码是否相等),field_validator 不方便,因为它一次只看到一个字段的值。这时用 @model_validator(mode="after"):它拿到的是整个模型对象,所有字段都齐了。
from pydantic import BaseModel, model_validator
class Account(BaseModel):
username: str
password1: str
password2: str
@model_validator(mode="after")
def passwords_match(self) -> "Account":
if self.password1 != self.password2:
raise ValueError("两次输入的密码不一致")
return self
mode="after" 的方法拿到的是 self,也就是整个实例,直接用 self.password1 读取。校验通过要 return self。
Note
mode="after"是实例方法,不需要@classmethod,也不要写cls。只有mode="before"才需要@classmethod和cls。
20-5 mode=“before” 的模型级校验
@model_validator(mode="before") 拿到的是原始输入,通常是一个字典。它适合在字段校验之前先改写或拦截数据。
from pydantic import BaseModel, model_validator
from typing import Any
class Profile(BaseModel):
nickname: str
@model_validator(mode="before")
@classmethod
def block_forbidden_field(cls, data: Any) -> Any:
if isinstance(data, dict) and "secret" in data:
raise ValueError("不允许提交 secret 字段")
return data
注意它必须配合 @classmethod,第一个参数是 cls,第二个 data 是原始数据。这种用法比 after 更灵活,但也更容易出错,初学阶段优先用 after。
20-6 抛出错误会被 FastAPI 接住
校验器抛出的 ValueError,最终会变成 Pydantic 的 ValidationError。在 FastAPI 里,框架会自动把它转成 422 响应,并告诉你错误位置。
from fastapi import FastAPI
from pydantic import BaseModel, field_validator
from typing import Any
class Signup(BaseModel):
username: str
age: int
@field_validator("username")
@classmethod
def check_username(cls, v: str) -> str:
if len(v) < 3:
raise ValueError("用户名至少 3 个字符")
return v
app = FastAPI()
@app.post("/signup/")
async def signup(data: Signup) -> dict[str, Any]:
return data.model_dump()
当客户端发来 {"username": "ab", "age": 20},FastAPI 返回类似:
{
"detail": [
{
"loc": ["body", "username"],
"msg": "Value error, 用户名至少 3 个字符",
"type": "value_error"
}
]
}
你看,错误带上了字段位置 body.username 和你写的提示信息。你完全不用手写 if 判断和错误响应。
NotePydantic v2(2.5 及以上)会自动给校验器抛出的
ValueError消息加上Value error,前缀,所以实际msg形如"Value error, 用户名至少 3 个字符"。你在代码里写原始文本即可,前缀是框架加的。
20-7 校验器里访问其他字段
如果非要在字段级校验里看别的字段,可以用第二个参数 info,通过 info.data 读取「已经校验过的」字段。但字段有先后顺序,只能读到排在它前面的字段。
from pydantic import BaseModel, field_validator, ValidationInfo
class Rectangle(BaseModel):
width: int
height: int
@field_validator("height")
@classmethod
def height_under_width(cls, v: int, info: ValidationInfo) -> int:
width = info.data.get("width")
if width is not None and v > width:
raise ValueError("高度不能超过宽度")
return v
Tip大多数跨字段需求,直接用
@model_validator(mode="after")更省心,因为那个时候所有字段都齐了,不用操心顺序。
20-8 结合外部上下文校验
有时校验要依赖外部信息,比如「查数据库看邮箱是否被占用」。Pydantic 提供 info.context,让你在调用 model_validate() 时临时塞入数据,校验器里再取出来用。
from pydantic import BaseModel, field_validator, ValidationInfo
def email_exists(email: str) -> bool:
# 这里只是示例,真实场景去查数据库
return email == "taken@example.com"
class Signup(BaseModel):
email: str
@field_validator("email")
@classmethod
def check_email_free(cls, v: str, info: ValidationInfo) -> str:
if info.context and info.context.get("check_db"):
if email_exists(v):
raise ValueError("邮箱已被注册")
return v
Signup.model_validate(
{"email": "taken@example.com"},
context={"check_db": True},
)
注意 context 是可选参数,不传时 info.context 是 None,校验器要自己判空。
20-9 两个最容易犯的错误
新手写校验器,八成会踩这两个坑。第一,忘记 return v。校验器必须把你处理后的值返回,否则该字段会变成 None,数据凭空消失。第二,直接 raise ValidationError(...)。校验器里只该抛 ValueError 或 AssertionError,Pydantic 会把它们包成漂亮的 ValidationError;你手动抛反而报错。
Tip如果只是想做断言,用
assert 条件, "提示"也行,它抛出的AssertionError同样会被 Pydantic 捕获。
20-10 一个完整的注册校验例子
把前面学的串起来:用户名限字母数字、密码不许有空格、两次密码一致。
from pydantic import BaseModel, field_validator, model_validator
class Register(BaseModel):
username: str
password1: str
password2: str
@field_validator("username")
@classmethod
def username_rule(cls, v: str) -> str:
if not v.isalnum():
raise ValueError("用户名只能含字母和数字")
return v
@field_validator("password1", "password2")
@classmethod
def password_no_space(cls, v: str) -> str:
if " " in v:
raise ValueError("密码不能含空格")
return v
@model_validator(mode="after")
def passwords_match(self) -> "Register":
if self.password1 != self.password2:
raise ValueError("两次密码不一致")
return self
这段逻辑清晰分层:单字段规则交给 field_validator,跨字段比对交给 model_validator,各司其职。
这里还有一个执行顺序需要记住:Pydantic 先按字段声明顺序跑完所有字段级校验,全部通过后才执行模型级校验。所以如果 username 本身就不合法,passwords_match 根本不会被调用。这个顺序对写法有实际影响——模型级校验里可以放心假设各字段的类型和基础约束都已成立,不必再判空或做类型转换。另一方面,若某个字段校验失败,Pydantic 并不会立刻中断,而是继续收集其余字段的错误,最后一次性把所有问题都放进 detail 列表返回,前端因此能一次看到全部校验提示,而不用反复试错。
20-11 小结
本章你学会了 Pydantic v2 的两种校验器:@field_validator 做单字段校验,默认 mode="after"、可设 mode="before" 清洗原始输入,还能同时管多个字段;@model_validator(mode="after") 做跨字段校验,拿到完整的 self。记住两个铁律——必须配合 @classmethod(model 的 after 除外),抛 ValueError 而不是 ValidationError,并且别忘了 return。下一章讲模型的配置与序列化。