Skip to content

FastAPI 契约:请求验证、Pydantic 2 与响应模型 ​

前置:函数与模块、异常、协议与资源。本系列以 Python 3.12、FastAPI 0.121.0、Pydantic 2.12.3 为教学基线,使用本地费用账本串联四篇。示例未执行;固定版本是为了说明机制,不能当成当前部署版本建议。

先确定输入来自哪里 ​

FastAPI如何区分路径参数和JSON请求体?路由中的 expense_id 来自路径参数,ExpenseInput 模型来自JSON请求体;依赖参数由 Depends 解析,不能把函数参数位置当作 HTTP 位置。先写接口表,再写类型标注,可避免客户端发 query 而服务端等 body 的错配。

本例 POST /expenses 的 body 必须同时包含 reference、category、cents;GET /expenses/{expense_id} 的 id 是1至2147483647的整数。URL 本来是文本,所以路径整数允许框架解析数字文本;body 的严格整数则拒绝 JSON 字符串。两者的输入载体和规则不同,不能全局把 strict=True 贴到路径参数并期望数字字符串仍可解析。

请求校验在调用路由业务前发生,但依赖解析属于同一请求准备过程;不要假设非法 body 一定不会打开依赖资源。数据库写入只放在路由内,校验失败不进入写事务。类型提示不是权限控制;任何人能接触该教学 API 都可读取删除账本,它没有认证系统。

Pydantic 2 的严格整数 ​

FastAPI怎样拒绝字符串金额和布尔金额?ExpenseInput 用 ConfigDict(strict=True) 及整数分 cents,使字符串金额、布尔金额与浮点金额(包括JSON的1.0)在严格整数验证边界失败;范围约束另由 Field(ge=1, le=100000000) 表达。严格类型和业务范围是两层检查,不能只写 int 就把金额上限交给数据库。

1250 表示12.50个货币单位,不把12.50当二进制浮点存入数据库。固定单币种且统一分单位是本例假设;若要支持三位小数货币,应修改契约和数据模型,而不是把分的含义悄悄更改。参考号限定1–40位 ASCII字母数字、短横线、下划线;分类限定 food/travel/other。extra="forbid" 拒绝调用方提交 id 等未知字段。

Pydantic 2 使用 model_validate、model_dump、ConfigDict;不要把旧版 dict()/parse_obj()/class Config 教程原样混入新代码。默认宽松模式会做部分类型转换,严格模式也不是所有输入格式完全相同,例如某些日期的 JSON 与 Python 对象规则不同。本例只为明确列出的 JSON 标量建立保证,见Pydantic严格模式。

请求错误与响应错误 ​

FastAPI的响应模型验证失败应该返回什么?请求错误属于调用方输入,本例统一422 invalid_request;响应模型失败属于服务器实现错误,不能伪装成422或返回空列表。response_model 描述并验证公开输出,帮助隔离数据库字段,但不是数据库事务,也不保证响应已经发到客户端。

ExpenseInput 不含 id,Expense 扩展 id;创建时显式构造 Expense,再提交事务,避免最常见的“提交后才发现返回字段不合法”。读取时 dict(row) 交给 response_model=Expense,若已有坏数据使模型失败,应调查数据来源并修复约束,不能在响应过滤器中默默删行。直接返回 Response 会绕过通常的模型序列化,本例只在空204删除成功时使用。

HTTPException(404, "not_found") 交由框架错误处理器生成 {"detail":"not_found"}。RequestValidationError 另有自定义处理器,只输出稳定代码,避免把用户输入或原始验证上下文回显出去。日志可记录脱敏后的错误类别;本例没有实现追踪或完整日志平台。依据:响应模型。

手写 OpenAPI 的地位 ​

FastAPI的手写OpenAPI能证明接口已经运行吗?openapi.json 是先于实现写下的设计契约,运行时生成结果尚未比较,不能证明接口可调用。它采用 OpenAPI 3.1,记录四个操作、请求 schema、成功状态及稳定错误形状;没有凭空生成客户端或假造 schema 对齐证据。

示例的静态契约与 FastAPI /openapi.json 是两个文件来源:前者由作者明确维护,后者依赖应用导入与框架生成。本轮限制不允许导入执行示例,因此只完成前者解析和逐项人工对照。未来由读者运行时应比较 paths、required、范围、additionalProperties、responses,不能仅看 Swagger UI 页面能打开。

OpenAPI 的 integer 能表达数值范围,却不总能在所有验证器中区分1与1.0;服务端严格模式的行为应同时写入说明和负例。静态 schema 也不能表达“最多100条记录”的并发事务过程、参考号冲突优先于容量冲突、无认证等全部语义。枚举 Error 指本例显式处理的错误;框架未知路由404/方法405及未捕获500不属于这份业务错误枚举保证。

版本与兼容边界 ​

FastAPI的依赖版本应该怎样核对?固定直接依赖并核对上游元数据,仍不等于传递依赖已锁定或安装验证。0.121.0 的官方 pyproject 声明 Starlette >=0.40,<0.50、Pydantic ❤️,并包含Python3.12分类;本例选择Pydantic2.12.3,不混用 pydantic.v1。

requirements.txt 同时固定 Uvicorn0.38.0 和测试客户端 HTTPX0.28.1,后续实际安装还会解析 AnyIO、Starlette 等传递依赖。没有 lock 文件,也没有声称跨平台可复现安装。依据:FastAPI 0.121.0项目元数据、Pydantic 2.12.3发布元数据、Uvicorn 0.38.0。

练习 ​

  1. 对比 cents=1、"1"、true、1.5、0,列出类型检查和范围检查各自负责的情况。
  2. 请求中加入 id=999;为什么忽略未知字段可能让调用者误以为自定义id生效?
  3. 让读取结果缺 cents;该返回422还是服务器错误?此时如何检查数据库已有坏数据?
  4. 只修改 OpenAPI 的金额上限而不改模型,会出现哪些客户端和服务端分歧?

参考答案 ​

1 通过;字符串、布尔和小数在严格整数边界拒绝;0类型正确但范围不合格。id 应因 extra="forbid" 得422,数据库生成id不会受调用方影响。缺cents属于响应模型错误,检查数据库约束、导入脚本和读取投影,不能改为“输入不对”。上限分歧可能导致客户端提前拒绝合法服务端输入,或客户端允许服务端拒绝的值;需同步契约、模型、数据库约束及边界负例,并在允许执行后比较实际schema。

继续:依赖与清理、完整项目、并发与故障。

继续阅读