代码语言

知识点思维导图

29 个知识节点

Python(15) - 请求参数与数据校验

读完后,你应能完成以下任务:

  • 绘制“Python(15) - 请求参数与数据校验 / 先建立直觉:Pydantic 模型 ≈ zod schema(不是 TS interface)”的关键对象与数据流,解释“边界(这里和 TS 不一样,最关键的认知):TS 的类型在编译后完全消失,运行时 age: number 不会拦住一个传进来的字符串。”,并用源码位置、日志或 Trace 标注证据。
  • 为“Python(15) - 请求参数与数据校验 / Pydantic 在 FastAPI 里到底管哪部分参数”设计正常与异常输入,验证“判断规则很简单:形参类型是 Pydantic 模型 → FastAPI 当成请求体;”,输出首个偏差位置与回归测试结果。
  • 实现“Python(15) - 请求参数与数据校验 / 字段约束:Field ≈ zod 的链式方法”的最小代码或配置,检验“注意必填的写法:在 Pydantic v2 里,「不给默认值」就是必填。”,输出命令、结果与 Diff,并说明不适用边界。

前端拿到一个表单,你会先用 zod 校验一遍再提交;后端接到请求,同样要先校验参数再处理,否则脏数据会一路烂到数据库。这篇讲 FastAPI 的校验主力 Pydantic——它长得像 TS interface,干的活像 zod,但有一个本质区别你必须先记住:它是运行时真校验,不是编译期摆设

一、先建立直觉:Pydantic 模型 ≈ zod schema(不是 TS interface)

你在前端定义一个「用户」的形状,大概率写过这两种:

// 写法 A:TS interface —— 只在编译期存在,打包后被擦除,运行时啥也不剩
interface User {
  name: string
  age: number
}

// 写法 B:zod schema —— 运行时真实存在的对象,能真的去校验数据
import { z } from 'zod'
const UserSchema = z.object({
  name: z.string().min(2),
  age: z.number().int().gte(0),
})
const user = UserSchema.parse(req.body)   // 不合法直接抛错

Pydantic 的 BaseModel 对应的是写法 B(zod),而不是写法 A:

维度 TS interface zod Pydantic BaseModel
运行时是否存在 否(被擦除)
能否真的校验数据 不能
长得像哪个 —— —— 形状像 interface,能力像 zod
类型不符时 编译期红线 运行时抛错 运行时抛 ValidationError

边界(这里和 TS 不一样,最关键的认知):TS 的类型在编译后完全消失,运行时 age: number 不会拦住一个传进来的字符串。Pydantic 的字段注解(age: int)是运行时生效的校验规则——它不仅校验,还会尝试自动转换(下面第三节细说)。所以别把 Pydantic 当成「Python 版 interface」,要当成「自带类型注解语法糖的 zod」。


二、Pydantic 在 FastAPI 里到底管哪部分参数

一个 HTTP 请求的参数有三个来源,FastAPI 对它们的处理方式不同。先建立全景图(路由、@app.post 这些写法详见第 14 篇 FastAPI 入门):

参数来源 例子 FastAPI 怎么接 谁来校验
路径参数 /users/100 里的 100 函数形参 user_id: int 类型注解直接校验
查询参数 /users?page=2&size=10 函数形参 page: int 类型注解 + Query()
请求体 POST 的 JSON body 形参类型是 Pydantic 模型 Pydantic 模型校验

判断规则很简单:形参类型是 Pydantic 模型 → FastAPI 当成请求体;是 int/str/float/bool 等基础类型 → 当成路径或查询参数

并排看你熟悉的 Express + zod,会发现 FastAPI 把「解析 + 校验」这步内建进了框架,省掉了手动 parse:

// Express 里你得自己解析、自己校验、自己 catch
app.post('/users/:userId', (req, res) => {
  const userId = Number(req.params.userId)          // 手动转类型
  const result = CreateUserSchema.safeParse(req.body) // 手动校验
  if (!result.success) return res.status(422).json(result.error)
  const body = result.data
  res.json({ userId, name: body.name })
})

FastAPI 把这三步(转类型、校验、出错返回 422)全自动化了,你只管声明类型。


三、字段约束:Field ≈ zod 的链式方法

zod 用链式调用堆约束,Pydantic 用 Field() 的参数堆约束。对照表:

校验意图 zod Pydantic Field()
字符串最小长度 .min(2) min_length=2
字符串最大长度 .max(20) max_length=20
数字 > 0 .gt(0).positive() gt=0
数字 >= 0 .gte(0) ge=0
数字 <= 100 .lte(100) le=100
正则匹配 .regex(/.../) pattern=r"..."
必填 默认必填 不给默认值即必填
可选/有默认值 .optional() / .default(x) 给默认值 = x

注意必填的写法:在 Pydantic v2 里,「不给默认值」就是必填。如果你想显式表达必填又想加约束,写 Field(...)(那个 ... 是 Python 的 Ellipsis 对象,Pydantic 约定它表示「必填」):


四、自动类型转换:和 TS 心智模型差最远的地方

这是前端最容易看走眼的点。Pydantic 在校验时会尝试合理转换,而不是死板地「类型不等就报错」。

为什么这很重要?因为 HTTP 查询参数和路径参数本质上全是字符串。前端 /users?age=18age 在 HTTP 层面是字符串 "18"。你在 FastAPI 里声明 age: int,Pydantic 自动把 "18" 转成 18——这正是你想要的,省掉了 Express 里满地的 Number(req.query.age)

WHY 它要自动转换:HTTP 协议传的全是文本,没有「数字类型」这一说。如果不自动转换,你每个接口都得手动 parse 一遍。Pydantic 替你做了这件脏活,这是它比「纯类型检查」更实用的地方。

边界提醒:自动转换是「合理范围内」的转换,不是 JS 那种激进的隐式转换。bool 字段传 "true"/"1"/1 能转成 True,但 Pydantic 不会像 JS 那样把空字符串 "" 当成 False、把 "hello" 强转成 truthy。规则比 JS 严格、可预测得多。


五、可选字段与「可空」:Optional 是个坑

前端 name?: string(可选)和 name: string | null(可空)是两件事,Python 这里更容易混。

前端写法 含义 对应 Pydantic
name?: string 可以不传 name: str = ""(给个同类型默认值)
name: string | null 必须传,但可以是 null name: Optional[str](不给默认值)
name?: string | null 可不传,传了可以是 null name: Optional[str] = None

Python 3.10+ 也可以用 str | None 代替 Optional[str],和 TS 的联合类型写法一模一样,更推荐。Optional 的细节详见第 11 篇类型注解。

可变默认值的坑(这是 Python 通用陷阱,Pydantic 里也会遇到):列表/字典类型的默认值不要直接写 = [],用 Field(default_factory=list)

Pydantic 实际上对 = [] 做了保护(会帮你深拷贝),但养成用 default_factory 的习惯更稳妥,也和普通 Python 代码一致。


六、嵌套模型与自定义校验

嵌套模型:模型字段的类型可以是另一个模型,对应前端嵌套的 zod schema:

自定义校验:约束不够用时(比如「确认密码要等于密码」),用 field_validator。它对应 zod 的 .refine()

对比 zod:

const RegisterSchema = z.object({
  username: z.string().refine(
    (v) => !['admin', 'root'].includes(v.toLowerCase()),
    { message: '该用户名为系统保留,不可使用' }
  ),
  password: z.string(),
})

注意 field_validator 必须配 @classmethod,且第一个参数是 cls 不是 self——这里它是类方法,不是实例方法(self/cls 的区别详见第 7 篇面向对象,核心是:self 不是 JS 的 this,得显式声明)。


七、响应也能校验:response_model

校验不只管「进来的」,也能管「出去的」。response_model 声明接口返回的形状,FastAPI 会按它过滤+校验响应数据。最典型的用途:从返回里抹掉密码等敏感字段

这相当于在出口处又套了一层「形状合同」,比手动 delete user.password 可靠得多。


八、Pydantic v1 vs v2:别被老教程带偏

网上大量 Pydantic 教程是 v1 的,API 名字变了。FastAPI 现在默认 v2,记住这几个高频改名,省得复制老代码报错:

用途 Pydantic v1(旧) Pydantic v2(现在用这个)
模型转 dict user.dict() user.model_dump()
模型转 JSON 字符串 user.json() user.model_dump_json()
从 dict 校验构造 User.parse_obj(d) User.model_validate(d)
自定义字段校验 @validator @field_validator
从 ORM 对象读取 Config.orm_mode = True model_config = {"from_attributes": True}

其中 from_attributes(旧名 orm_mode)在下一阶段对接数据库时会用到——让 Pydantic 模型能直接从 SQLAlchemy 的 ORM 对象读字段(详见第 16 篇数据库操作)。

另外 EmailStr(邮箱专用类型)需要额外装包 pip install email-validator 才能用,否则会报错。不想装就先用普通 str + 正则 pattern


九、总结

  • 先建立直觉:Pydantic 模型 ≈ zod schema(不是 TS interface):| 运行时是否存在 | 否(被擦除) | 是 | 是 |
  • Pydantic 在 FastAPI 里到底管哪部分参数:| 请求体 | POST 的 JSON body | 形参类型是 Pydantic 模型 | Pydantic 模型校验 |
  • 字段约束:Field ≈ zod 的链式方法:注意必填的写法:在 Pydantic v2 里,「不给默认值」就是必填。
  • 自动类型转换:和 TS 心智模型差最远的地方:这是前端最容易看走眼的点。
  • 可选字段与「可空」:Optional 是个坑:前端 name?: string(可选)和 name: string | null(可空)是两件事,Python 这里更容易混。
  • 嵌套模型与自定义校验:注意 field_validator 必须配 @classmethod,且第一个参数是 cls 不是 self——这里它是类方法,不是实例方法(self/cls 的区别详见第 7 篇面向对象,核心是:self 不是 JS 的 this,得显式声明)。

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“请求参数与数据校验”中,需要同时满足“先建立直觉:Pydantic 模型 ≈ zod schema(不是 TS interface)”与“Pydantic 在 FastAPI 里到底管哪部分参数”。给定正文约束“Pydantic 的 BaseModel 对应的是写法 B(zod),而不是写法 A。”,哪些判断保持了原有处理机制?多选
2“请求参数与数据校验”出现偏差:“在“请求参数与数据校验 / 字段约束:Field ≈ zod 的链式方法”中,即使不满足“zod 用链式调用堆约束,Pydantic 用 Field() 的参数堆约束”,结果与副作用仍会保持不变。”已成为实际行为。围绕“字段约束:Field ≈ zod 的链式方法”与“自动类型转换:和 TS 心智模型差最远的地方”,哪些判断能定位被改变的职责或边界?多选
3评审“请求参数与数据校验”方案时,验收条件包含“前端 name?: string(可选)和 name: string | null(可空)是两件事,Python 这里更容易混。”。关于“可选字段与「可空」:Optional 是个坑”与“嵌套模型与自定义校验”的哪些决策符合正文机制?多选