知识点思维导图
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=18,age 在 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,得显式声明)。
学完自测
选择所有正确答案;提交后逐项核对判断依据。