首页 / FastAPI 入门教程 / 校验器

FastAPI 入门教程

校验器

本教程共 50 篇 · 第 20 篇 · 更新于 2026-08-12 · 约 7 分钟阅读

FastAPIFastAPI 入门教程Pydantic v2field_validatormodel_validator自定义校验

本节目标:学会用 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 约定你抛普通的 ValueErrorAssertionError,它会自动包成漂亮的校验错误。

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

这样 passwordconfirm 都会被检查。不过要注意,字段是按声明顺序逐个校验的,跨字段比对时优先考虑下面的 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" 才需要 @classmethodcls

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 判断和错误响应。

Note

Pydantic 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.contextNone,校验器要自己判空。

20-9 两个最容易犯的错误

新手写校验器,八成会踩这两个坑。第一,忘记 return v。校验器必须把你处理后的值返回,否则该字段会变成 None,数据凭空消失。第二,直接 raise ValidationError(...)。校验器里只该抛 ValueErrorAssertionError,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。下一章讲模型的配置与序列化。