在请求模型里看到 @field_validator("score"),很容易以为它只是给方法加了一个标记:这个方法由谁调用?返回的值又去了哪里?
它的作用是告诉 Pydantic:校验 score 字段时,自动执行下面的方法,并把返回值作为这个字段的最终值。 本文适合已经会写 BaseModel、但还不熟悉自定义校验的读者,示例使用 Pydantic v2。
类型正确,还不够
假设接口接收一次考试成绩:
{
"score": 120
}
int 可以说明成绩应当是整数,但 120 也是整数。对于满分 100 分的考试,还需要检查成绩是否在 0~100 之间。
这些规则可以放进字段校验器,在请求进入业务处理前统一完成。
一个可以直接运行的例子
把下面代码保存为 example.py,通过 uv run --with 'pydantic>=2,<3' python example.py 执行:
from pydantic import BaseModel, ValidationError, field_validator
class ExamResult(BaseModel):
score: int
@field_validator("score")
@classmethod
def validate_score(cls, value: int) -> int:
if not 0 <= value <= 100:
raise ValueError("成绩必须在 0~100 之间")
return value
result = ExamResult(score=85)
assert result.score == 85
print(result.score)
try:
ExamResult(score=120)
except ValidationError as exc:
error = exc.errors()[0]
assert error["loc"] == ("score",)
print(error["msg"])
else:
raise AssertionError("应该拒绝超过满分的成绩")
输出:
85
Value error, 成绩必须在 0~100 之间
本例是为了观察校验器的执行过程。实际只有范围限制时,直接写 score: int = Field(ge=0, le=100) 更简单,记得从 pydantic 导入 Field。遇到内置约束无法直接表达的规则时,再编写自定义校验器。
这段代码什么时候执行?
调用 ExamResult(...) 时,Pydantic 先按 int 处理字段,再调用 validate_score()。这是 field_validator 默认的 mode="after" 行为。
因此,这个方法拿到的 value 已经是通过基础校验的整数。例如默认非严格模式下,输入 score="85" 会先被转换成整数 85,再进行范围检查。
方法返回的值成为 result.score。调用方不需要再手动执行 validate_score()。
这里要区分两个装饰器的职责:
@field_validator("score"):向 Pydantic 注册这个字段的校验方法。@classmethod:让方法接收类cls,而不是实例self;本例不需要使用cls。
如果类型校验已经失败,例如传入 score="abc",默认的 after 校验器不会继续处理这个字段。
为什么抛出 ValueError,外面却捕获 ValidationError?
在校验器内部,ValueError 表示“这个值不符合要求”。Pydantic 会收集它,并生成带有字段位置的 ValidationError,所以调用方能知道是哪个字段出错。
如果这个模型作为 FastAPI 的请求体,正常的请求校验失败默认返回 HTTP 422;项目自定义异常处理后,响应格式和状态码可能不同。Pydantic 本身只负责数据校验,不负责 HTTP 响应。
使用时记住三个边界
校验成功后要返回值。 即使没有修改输入,也应该 return value。遗漏返回值会让方法返回 None,不能指望 Pydantic 自动保留原值。
after 与 before 不同。 mode="before" 在基础类型校验之前执行,拿到的可能是任意原始输入,不能直接假设是整数。这里没有提前处理原始输入的需求,默认 after 就够了。
模型校验不等于持续监视对象。 正常构造或调用 model_validate() 会执行校验;默认情况下,之后直接执行 result.score = 120 不会重新校验。需要赋值时也校验,可以显式配置 validate_assignment=True。
可以试着把输入分别改成 0、100、-1 和 "abc",观察哪些值能通过,以及错误来自基础类型还是自己的校验方法。