FastAPI 实战:用 Pydantic 完善学生接口的增删改模型
上一节我们定义了 Student 数据模型,并把它用到了“获取全部学生”和“获取单个学生”两个接口上。这一节继续把 Pydantic 模型接入到添加学生和更新学生接口中,让接口对客户端传入的数据有真正的校验能力。
本文完整链路:发现问题 → 新建模型 → 改造接口 → 测试校验 → 修正报错 → 结果验证 → 小结。
一、问题:添加接口对数据毫无约束
先看原来的 create student 接口,请求体类型是普通的 dict,意味着客户端传什么都能进来。
在 Swagger 文档里点开添加接口,随便填几个不相关的字段(比如 name 和 age),点击 Execute,接口依然返回成功,数据被直接写入。这说明当前接口没有做任何模型校验。
我们需要让客户端只能传“有效字段”,因此要引入模型约束。
二、为什么不能直接复用 Student 模型
直觉上可以让添加接口直接使用 Student 模型,但仔细看字段就会发现不行:
id:数据库中通常是自增的,不能让用户填写;- 创建时间等字段:由数据库系统在插入时自动生成,也不应由用户传入。
所以必须单独定义一个创建专用的模型。
三、新建 StudentCreate 模型
新建 StudentCreate 类,同样继承 BaseModel,字段从 Student 拷贝过来,但去掉 id(以及创建时间等由系统维护的字段)。
class StudentCreate(BaseModel):
name: str
student_no: str
gender: str
birthday: datetime.date | None = None
phone: str
class_id: int
接着改造 POST 接口:
- 请求体类型由
dict改为StudentCreate; - 通过
response_model指定返回结构为Student。
@app.post("/students", response_model=Student)
def create_student(student: StudentCreate):
...
完整代码如下:
@app.post("/api/v1/students", tags=["学生管理"],response_model=Student,status_code=201)
def create_student(student: StudentCreate):
# 判断学号是否存在
for s in fake_students_db:
if s["student_no"] == student.student_no:
raise HTTPException(status_code=400, detail=f"学号{student.student_no}已经存在")
# 添加学生信息
new_id = max(s["id"] for s in fake_students_db) + 1
new_student = {"id": new_id, **student.model_dump()}
fake_students_db.append(new_student)
return new_student
四、测试:缺字段会被拦截
在 Swagger 中只填 name 和 age 提交,接口返回 422,不再是 200。查看 detail 中的信息,会逐条列出缺失字段:
gender缺失;phone缺失;class_id缺失。
注意 birthday 没有报缺失,因为它在模型中被定义为可为空(Optional),不传也不会报错。
刷新接口文档,能看到新增的 StudentCreate 数据结构:带星号的字段为必填,birthday 没有星号,表示可选。错误信息里还会给出示例值(example value),比原来的 dict 友好得多。
五、修正:请求体已是模型对象,不能再当字典用
填写完整数据再次提交,却报了服务器错误:
'StudentCreate' object has no attribute 'get'
原因是请求体 student 现在是 StudentCreate 类的实例,不再是字典,不能再用 student.get(...) 和 **student 拆包。
改为直接访问类属性,并用 model_dump() 转成字典:
student.model_dump()
修改后刷新页面,重新提交一条完整数据,添加成功,校验也不再报错。
再故意传一个不存在的 age 字段测试,接口返回 422,提示 student_no、gender、class_id 等字段缺失;birthday 依旧不提示,因为它是可选项。
六、更新接口:新建 StudentUpdate 模型
更新场景有个痛点:如果只想改手机号,难道要把所有字段重填一遍?不填的话又会被整体覆盖成空值。
解决办法是新建 StudentUpdate 模型,继承 BaseModel,把每个字段都设为可选(默认 None),实现“填就改、不填就不动”。
另外,学号在真实业务中不允许用户随意修改,所以 StudentUpdate 中直接删掉学号字段。
class StudentUpdate(BaseModel):
name: str | None = None
gender: str | None = None
birthday: datetime.date | None = None
phone: str | None = None
class_id: int | None = None
改造 PUT 接口:
- 请求体类型改为
StudentUpdate; - 用
model_dump(exclude_unset=True)转字典,只保留用户实际提交的字段; - 通过
response_model返回修改后的完整学生信息。
@app.put("/students/{student_id}", response_model=Student)
def update_student(student_id: int, student: StudentUpdate):
data = student.model_dump(exclude_unset=True)
...
完整代码如下:
@app.put("/api/v1/students/{student_id}", response_model=Student, tags=["学生管理"])
def update_student(student_id: int, student: StudentUpdate):
for index, s in enumerate(fake_students_db):
if s["id"] == student_id:
# 获取更改后的学生信息
updated = {**s, **student.model_dump(exclude_unset=True)}
fake_students_db[index] = updated
return updated
# 找不到抛出异常
raise HTTPException(status_code=404, detail=f"学生ID为{student_id} 不存在")
七、验证 exclude_unset 的作用
刷新文档,在 PUT 接口里只把 name 改成 indie 后提交(第一次因 JSON 里多了个双引号报格式错误,删掉后重试)。
返回码 200,name 变为 indie,其余字段保持原值不变,说明 exclude_unset=True 生效了。
反向验证:把 exclude_unset=True 去掉再提交,接口报 500 服务器错误。查看 FastAPI 日志是验证异常——未填写的 gender、phone、class_id 全部变成 None,与原有字段类型不符导致报错。把参数加回去,更新接口恢复正常。
八、删除接口无需改动
删除学生接口没有返回值,只接收一个 id 路径参数,也不涉及请求体数据校验,因此不需要设置 response_model,保持原样即可。
小结
本节用 Pydantic 构建了三个数据模型:
| 模型 | 用途 | 关键点 |
|---|---|---|
Student | 学生完整信息 | 用于查询与响应 |
StudentCreate | 创建学生 | 去掉 id 等系统字段,约束客户端输入 |
StudentUpdate | 更新学生 | 所有字段可选,去掉不可改的学号 |
两个要点:
- 请求体变成模型实例后,不能再按字典调用
get()或**拆包,统一用model_dump()转字典。 - 更新接口必须加
exclude_unset=True,只更新用户提交的字段,未提交字段保持原值,避免被覆盖成None。
至此,学生接口的模型校验全部完成。
想系统学习这门课程?
你已阅读完本文。需要更完整的知识体系,可以继续了解「FastAPI 项目实战」。
查看课程详情