写接口最怕什么?不是逻辑复杂,而是脏数据悄悄进了数据库。FastAPI 与 Pydantic V2 深度集成,支持在请求体进入路由函数之前就完成校验------Field 约束、field_validator 单字段校验、model_validator 跨字段校验,三层防线层层拦截。再加上 response_model 自动过滤返回字段,连安全漏洞(如密码泄露)都能防住。本文完整演示这三种校验方式的实际用法。
目录
- [1. 三层校验体系总览](#1. 三层校验体系总览)
- [2. 第一层:Field 基础约束](#2. 第一层:Field 基础约束)
- [3. 第二层:field_validator 单字段校验](#3. 第二层:field_validator 单字段校验)
- [4. 第三层:model_validator 跨字段校验](#4. 第三层:model_validator 跨字段校验)
- [5. response_model 响应过滤](#5. response_model 响应过滤)
- [6. 总结](#6. 总结)
1. 三层校验体系总览
Pydantic V2 提供了三道防线,按执行顺序排列:
| 防线 | 工具 | 作用范围 | 典型场景 |
|---|---|---|---|
| 第一层 | Field() |
单字段基础约束 | 长度、范围、正则、必填/可选 |
| 第二层 | @field_validator |
单字段自定义逻辑 | 中英文区分、枚举校验、格式化 |
| 第三层 | @model_validator |
跨字段联合校验 | 密码确认、日期范围、条件必填 |
一句话总结:Field 管基础格式,field_validator 管单字段业务规则,model_validator 管字段间联动关系,三层递进、各司其职。
2. 第一层:Field 基础约束
Field 是 Pydantic 内置的约束声明,一行代码完成常见校验:
python
from pydantic import BaseModel, Field
from typing import Optional
class UserCreateVo(BaseModel):
"""创建用户请求体"""
name: str = Field(..., min_length=1, max_length=50, description="用户名")
age: int = Field(..., ge=0, le=150, description="年龄")
email: Optional[str] = Field(None, pattern=r"^\S+@\S+\.\S+$", description="邮箱")
class ShopCreateVo(BaseModel):
"""创建购物车请求体"""
name: str = Field(..., min_length=1, max_length=100, description="购物车名称")
counter: int = Field(..., ge=0, le=10, description="数量")
price: float = Field(..., ge=0, description="价格")
Field 常用约束参数一览:
| 参数 | 作用 | 示例 |
|---|---|---|
... (Ellipsis) |
必填字段 | Field(...) |
None |
可选字段 | Field(None) |
min_length / max_length |
字符串长度 | min_length=1, max_length=50 |
ge / le |
数值范围(>= / <=) | ge=0, le=150 |
gt / lt |
数值范围(> / <) | gt=0 |
pattern |
正则匹配 | pattern=r"^\S+@\S+\.\S+$" |
description |
字段说明(显示在Swagger) | description="用户名" |
Optional[str] 等价于 str | None,表示该字段可以不传,默认值为 None。而 Field(...) 中的 ... 表示该字段必须传,没有默认值。
一句话总结:Field 约束是最轻量的校验方式,能用 Field 解决的别写 validator,代码越少 Bug 越少。
3. 第二层:field_validator 单字段校验
当 Field 的基础约束不够用时,@field_validator 登场。它可以实现跨 Field 参数无法表达的复杂逻辑。
python
from pydantic import BaseModel, Field, field_validator
class RegisterVo(BaseModel):
"""注册请求体:演示 field_validator"""
username: str = Field(..., description="用户名")
password: str = Field(..., min_length=6, max_length=20, description="密码")
age: int = Field(..., description="年龄")
@field_validator("username")
@classmethod
def username_must_valid(cls, v: str) -> str:
# 中文名至少2字,英文名至少3字母
if v.isascii():
if len(v) < 3:
raise ValueError("英文用户名至少需要 3 个字符")
else:
if len(v) < 2:
raise ValueError("中文用户名至少需要 2 个字符")
return v.strip()
@field_validator("age")
@classmethod
def age_must_reasonable(cls, v: int) -> int:
if v < 0 or v > 150:
raise ValueError("年龄必须在 0-150 之间")
return v
三个关键语法点:
@field_validator("字段名")指定要校验的字段,可以写多个字段名- 必须是
@classmethod,第一个参数cls是类本身,第二个参数v是字段值 - 返回值会替换原值 (比如
v.strip()去掉了用户名前后空格)
效果演示 :传入 {"username": "ab", "password": "123456", "age": 25} 会报错 "英文用户名至少需要 3 个字符"。传入 {"username": "张三", ...} 则通过,因为中文只需 2 个字。
一句话总结:field_validator 让你在字段级别写任意复杂的校验逻辑,校验失败直接抛 ValueError,FastAPI 会自动转为 422 错误响应。
4. 第三层:model_validator 跨字段校验
有些校验需要比较多个字段之间的关系,比如"两次密码是否一致"。这时用 @model_validator(mode="after"):
python
from pydantic import BaseModel, Field, model_validator
class SetPasswordVo(BaseModel):
"""修改密码请求体:演示 model_validator 跨字段校验"""
password: str = Field(..., min_length=6, max_length=20)
confirm_password: str = Field(..., min_length=6, max_length=20)
@model_validator(mode="after")
def passwords_match(self):
if self.password != self.confirm_password:
raise ValueError("两次密码不一致")
return self
两个 mode 的区别:
| mode | 执行时机 | self 是什么 | 使用场景 |
|---|---|---|---|
"before" |
字段校验之前 | 原始输入 dict | 数据预处理、字段名转换 |
"after" |
字段校验之后 | 已构造的 Pydantic 实例 | 跨字段比较、联合校验 |
为什么用 mode="after" ?因为密码确认的场景需要先确保两个字段各自通过了 Field 校验(长度 6-20),再比较是否一致。如果用 mode="before",可能拿到的是还没校验过的原始值。
一句话总结:model_validator(mode="after") 是处理跨字段校验的利器,"两次密码是否一致""结束时间是否大于开始时间"这类场景的首选方案。
5. response_model 响应过滤
校验只管"进",response_model 管"出"------它决定了返回给客户端的数据长什么样。
python
# 定义响应模型:只暴露安全字段
class UserResponse(BaseModel):
id: int
name: str
age: int
email: Optional[str] = None
# 注意:没有 password_hash 字段!
# 路由中使用 response_model
@user_route.get("/userPage", response_model=list[UserResponse])
def list_users(page: int = Query(1, ge=1), size: int = Query(10, ge=1, le=100),
db: Session = Depends(get_db)):
return user_service.get_users_page(db, offset, size)
response_model 做了什么:
- 即使
UserORM 对象有password_hash字段,response_model=list[UserResponse]会自动过滤掉它 - 如果 ORM 对象缺少
UserResponse中定义的字段,校验会失败,帮你发现数据问题 list[UserResponse]表示返回一个数组,每个元素都按UserResponse过滤
这是一个容易被忽视的安全实践:很多开发者直接把 ORM 对象返回给客户端,导致 password_hash 等敏感字段泄露。response_model 用声明式的方式杜绝了这个问题。
一句话总结:response_model 既是文档(告诉前端返回什么),又是过滤器(自动剔除多余字段),还是安全防线(防止敏感数据泄露),一举三得。
6. 总结
Pydantic V2 的三层校验体系覆盖了 API 开发中绝大多数的数据校验场景:
| 层级 | 工具 | 解决什么问题 |
|---|---|---|
| Field 约束 | Field(min_length=, ge=, pattern=) |
基础格式校验,最常见的需求 |
| 单字段校验 | @field_validator |
字段级别的复杂业务规则 |
| 跨字段校验 | @model_validator |
字段之间的联动关系 |
| 响应过滤 | response_model |
控制输出,防止敏感数据泄露 |
如果用一句话总结:Pydantic V2 的三层校验 + response_model 组合,让你用声明式的代码实现从前到后的数据安全保障,脏数据根本进不了业务逻辑层。