请求与响应校验
请求与响应校验 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
P07 的接口只收查询参数,这一课给「新增笔记」加上真正的写入口:用 Pydantic 模型校验请求体、用 response_model 控制输出结构,让缺字段、超长、非法枚举在门口就被拦下。
问题场景
前端要新增一条学习笔记:POST /notes,body 是 {title, body, tag}。用户手滑提交了空标题、一万字的正文、或者 tag 写成了 notes(复数)。P07 的写法里这些都是裸 dict 直接进函数——校验逻辑要自己写一坨 if。Pydantic 模型把这些 if 声明成了数据。
前端对应物你已经很熟:zod schema + safeParse。Pydantic 在 FastAPI 里扮演的就是这个角色,而且和路由函数长在一起。
核心解释
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import Literal
app = FastAPI()
class NoteIn(BaseModel): # 输入模型:决定「什么能进来」
title: str = Field(min_length=1, max_length=60)
body: str = Field(min_length=1, max_length=5000)
tag: Literal["course", "note", "review"] = "note"
class NoteOut(BaseModel): # 输出模型:决定「什么能出去」
id: str
title: str
tag: str
NOTES: list[dict] = []
@app.post("/notes", response_model=NoteOut, status_code=201)
def create_note(note: NoteIn):
if any(n["title"] == note.title for n in NOTES):
raise HTTPException(status_code=409, detail="同名笔记已存在")
item = {"id": f"note_{len(NOTES)+1}", **note.model_dump(), "internal": "secret"}
NOTES.append(item)
return item # response_model 会过滤掉未声明字段
四条机制看清再动手:
- 校验在进函数之前。缺
title、正文超 5000 字、tag不在白名单,FastAPI 直接返回 422,函数体一行都不执行——和 zod 在数据层拦截同理,只是这里连路由层都省了。 - 约束写在字段上。
Field(min_length=..., max_length=...)管长度,Literal/Enum管枚举,ge/le管数值范围。错误信息里会精确指出哪个字段、什么原因。 - response_model 是出口过滤。函数里多塞的
internal字段不会出现在响应里;出参和入参分开声明,内部字段就永远不会泄漏。 - 业务错误仍用 HTTPException。模型管「形状」,重复写入这类业务冲突还是你自己在函数里抛 409——校验框架管不了业务规则。
示例拆解
用真实请求把三个失败场景打一遍:
uvicorn main:app --reload --port 8100
curl -X POST http://127.0.0.1:8100/notes -H "Content-Type: application/json" ^
-d "{\"title\":\"函数与模块\",\"body\":\"拆函数心得\",\"tag\":\"note\"}"
# 201,返回 {"id":"note_1","title":"函数与模块","tag":"note"} —— internal 被过滤
curl -X POST http://127.0.0.1:8100/notes -H "Content-Type: application/json" ^
-d "{\"title\":\"\",\"body\":\"x\"}"
# 422,detail 指出 title 至少 1 个字符
curl -X POST http://127.0.0.1:8100/notes -H "Content-Type: application/json" ^
-d "{\"title\":\"a\",\"body\":\"x\",\"tag\":\"memo\"}"
# 422,tag 不是合法枚举值
预期:三种情况行为各不相同——合法的 201、形状错的 422、业务冲突的 409。422 响应体里 loc(哪个字段)和 msg(什么问题)是成对出现的,前端可以直接映射成表单错误提示。
动手任务
- 给
NoteIn加minutes: int | None = Field(default=None, ge=0, le=600),表示关联学习时长,用/docs验证越界值被拒。 - 新增
GET /notes返回list[NoteOut],确认 internal 字段在列表里同样被过滤。 - 把
tag从Literal改成Enum类实现,对比两种写法的错误信息,选一个留下并说明理由。
常见错误
- 在函数里手写 if 校验:模型已经声明过的约束又写一遍,两边迟早不一致;形状约束全交模型。
- 入参出参共用一个模型:内部字段一不小心就随响应漏出去;In/Out 模型分开。
- 把 422 当 bug 修:422 是校验在工作,先看
loc/msg修正请求,别急着改服务端。 - Field 范围拍脑袋:max_length 定得太大会拖垮存储层,太小挡掉正常输入;参照真实数据分布定。
检查与复盘
自查清单:
- 请求体字段都有长度/范围/枚举约束,且能在 /docs 里验证
- 能说清 422(形状错)和 409(业务冲突)的分工
- 响应里找不到任何未在 Out 模型声明的字段
参考资料
- FastAPI 官方教程:Request Body —— 请求体模型基础
- FastAPI 官方教程:Response Model —— 输出过滤与 status_code
- Pydantic 官方文档:Fields —— 约束参数全集
读完这一课了?
标记完成后,工作台会安排接下来的学习。