【FastAPI 全栈实战 | 第2篇】Pydantic 数据校验与响应模型 —— 让 Bug 死在请求进来的那一刻

写接口最怕什么?不是逻辑复杂,而是脏数据悄悄进了数据库。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

三个关键语法点

  1. @field_validator("字段名") 指定要校验的字段,可以写多个字段名
  2. 必须是 @classmethod,第一个参数 cls 是类本身,第二个参数 v 是字段值
  3. 返回值会替换原值 (比如 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 做了什么

  • 即使 User ORM 对象有 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 组合,让你用声明式的代码实现从前到后的数据安全保障,脏数据根本进不了业务逻辑层。


相关推荐
小林ixn1 小时前
远程 MCP 实战:让 AI 无缝调用地图、浏览器与文件系统
人工智能·agent·mcp
weixin_422329311 小时前
AgentScope Java2.0 版本Builder 本地启动指南
人工智能
studyrunner1 小时前
GPT-5.5 对比 GPT-5.6 Sol、Terra、Luna:官方性能数据与选型分析
大数据·人工智能·gpt
Xiangchu_1 小时前
安徽硅胶厂的转型困局:从家电到汽车,卡在哪
经验分享·python·阿里云·eclipse·汽车·制造
OpenMiniServer1 小时前
大统一逻辑链3.0(GULP3.0):绝对悖论、逻辑链与宇宙演化动力学
人工智能
Asize1 小时前
Agent 入门:从 LLM 与 Agent 的区别到 Function Calling
javascript·人工智能
小林ixn1 小时前
AI Agent的“万能工具箱”:MCP协议实战,让工具调用不再受语言和进程束缚
人工智能·agent·mcp
武子康1 小时前
🔥 GPT-5.6 有限预览全拆解:Sol/Terra/Luna 三档定价 + max/ultra 双推理 + 缓存 1.00×/1.25×/0.10× 三倍
人工智能·chatgpt·openai
CTA终结者1 小时前
近期AI量化工具推荐,围绕最难推进的环节选择
人工智能·python