【FastAPI 全栈实战 | 第2篇】Pydantic 数据校验与响应模型 —— 让 Bug 死在请求进来的那一刻
写接口最怕什么不是逻辑复杂而是脏数据悄悄进了数据库。FastAPI 与 Pydantic V2 深度集成支持在请求体进入路由函数之前就完成校验——Field 约束、field_validator 单字段校验、model_validator 跨字段校验三层防线层层拦截。再加上 response_model 自动过滤返回字段连安全漏洞如密码泄露都能防住。本文完整演示这三种校验方式的实际用法。目录1. 三层校验体系总览2. 第一层Field 基础约束3. 第二层field_validator 单字段校验4. 第三层model_validator 跨字段校验5. response_model 响应过滤6. 总结1. 三层校验体系总览Pydantic V2 提供了三道防线按执行顺序排列防线工具作用范围典型场景第一层Field()单字段基础约束长度、范围、正则、必填/可选第二层field_validator单字段自定义逻辑中英文区分、枚举校验、格式化第三层model_validator跨字段联合校验密码确认、日期范围、条件必填一句话总结Field 管基础格式field_validator 管单字段业务规则model_validator 管字段间联动关系三层递进、各司其职。2. 第一层Field 基础约束Field 是 Pydantic 内置的约束声明一行代码完成常见校验frompydanticimportBaseModel,FieldfromtypingimportOptionalclassUserCreateVo(BaseModel):创建用户请求体name:strField(...,min_length1,max_length50,description用户名)age:intField(...,ge0,le150,description年龄)email:Optional[str]Field(None,patternr^\S\S\.\S$,description邮箱)classShopCreateVo(BaseModel):创建购物车请求体name:strField(...,min_length1,max_length100,description购物车名称)counter:intField(...,ge0,le10,description数量)price:floatField(...,ge0,description价格)Field 常用约束参数一览参数作用示例...(Ellipsis)必填字段Field(...)None可选字段Field(None)min_length/max_length字符串长度min_length1, max_length50ge/le数值范围 / ge0, le150gt/lt数值范围 / gt0pattern正则匹配patternr^\S\S\.\S$description字段说明显示在Swaggerdescription用户名Optional[str]等价于str | None表示该字段可以不传默认值为None。而Field(...)中的...表示该字段必须传没有默认值。一句话总结Field 约束是最轻量的校验方式能用 Field 解决的别写 validator代码越少 Bug 越少。3. 第二层field_validator 单字段校验当 Field 的基础约束不够用时field_validator登场。它可以实现跨 Field 参数无法表达的复杂逻辑。frompydanticimportBaseModel,Field,field_validatorclassRegisterVo(BaseModel):注册请求体演示 field_validatorusername:strField(...,description用户名)password:strField(...,min_length6,max_length20,description密码)age:intField(...,description年龄)field_validator(username)classmethoddefusername_must_valid(cls,v:str)-str:# 中文名至少2字英文名至少3字母ifv.isascii():iflen(v)3:raiseValueError(英文用户名至少需要 3 个字符)else:iflen(v)2:raiseValueError(中文用户名至少需要 2 个字符)returnv.strip()field_validator(age)classmethoddefage_must_reasonable(cls,v:int)-int:ifv0orv150:raiseValueError(年龄必须在 0-150 之间)returnv三个关键语法点field_validator(字段名)指定要校验的字段可以写多个字段名必须是classmethod第一个参数cls是类本身第二个参数v是字段值返回值会替换原值比如v.strip()去掉了用户名前后空格效果演示传入{username: ab, password: 123456, age: 25}会报错英文用户名至少需要 3 个字符。传入{username: 张三, ...}则通过因为中文只需 2 个字。一句话总结field_validator 让你在字段级别写任意复杂的校验逻辑校验失败直接抛 ValueErrorFastAPI 会自动转为 422 错误响应。4. 第三层model_validator 跨字段校验有些校验需要比较多个字段之间的关系比如两次密码是否一致。这时用model_validator(modeafter)frompydanticimportBaseModel,Field,model_validatorclassSetPasswordVo(BaseModel):修改密码请求体演示 model_validator 跨字段校验password:strField(...,min_length6,max_length20)confirm_password:strField(...,min_length6,max_length20)model_validator(modeafter)defpasswords_match(self):ifself.password!self.confirm_password:raiseValueError(两次密码不一致)returnself两个 mode 的区别mode执行时机self 是什么使用场景before字段校验之前原始输入 dict数据预处理、字段名转换after字段校验之后已构造的 Pydantic 实例跨字段比较、联合校验为什么用modeafter因为密码确认的场景需要先确保两个字段各自通过了 Field 校验长度 6-20再比较是否一致。如果用modebefore可能拿到的是还没校验过的原始值。一句话总结model_validator(mode“after”) 是处理跨字段校验的利器“两次密码是否一致”结束时间是否大于开始时间这类场景的首选方案。5. response_model 响应过滤校验只管进response_model管出——它决定了返回给客户端的数据长什么样。# 定义响应模型只暴露安全字段classUserResponse(BaseModel):id:intname:strage:intemail:Optional[str]None# 注意没有 password_hash 字段# 路由中使用 response_modeluser_route.get(/userPage,response_modellist[UserResponse])deflist_users(page:intQuery(1,ge1),size:intQuery(10,ge1,le100),db:SessionDepends(get_db)):returnuser_service.get_users_page(db,offset,size)response_model 做了什么即使UserORM 对象有password_hash字段response_modellist[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 组合让你用声明式的代码实现从前到后的数据安全保障脏数据根本进不了业务逻辑层。