跳到主要内容

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):
...
新建 StudentCreate 模型并改造 POST 接口

完整代码如下:

@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 友好得多。

缺字段提交返回 422 并列出缺失字段

五、修正:请求体已是模型对象,不能再当字典用​

填写完整数据再次提交,却报了服务器错误:

'StudentCreate' object has no attribute 'get'

原因是请求体 student 现在是 StudentCreate 类的实例,不再是字典,不能再用 student.get(...) 和 **student 拆包。

改为直接访问类属性,并用 model_dump() 转成字典:

student.model_dump()

修改后刷新页面,重新提交一条完整数据,添加成功,校验也不再报错。

使用 model_dump 转换后添加学生成功

再故意传一个不存在的 age 字段测试,接口返回 422,提示 student_no、gender、class_id 等字段缺失;birthday 依旧不提示,因为它是可选项。

传入非法字段返回 422 缺失提示

六、更新接口:新建 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)
...
新建 StudentUpdate 模型并改造 PUT 接口

完整代码如下:

@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 生效了。

只改 name 其他字段保持不变

反向验证:把 exclude_unset=True 去掉再提交,接口报 500 服务器错误。查看 FastAPI 日志是验证异常——未填写的 gender、phone、class_id 全部变成 None,与原有字段类型不符导致报错。把参数加回去,更新接口恢复正常。


八、删除接口无需改动​

删除学生接口没有返回值,只接收一个 id 路径参数,也不涉及请求体数据校验,因此不需要设置 response_model,保持原样即可。


小结​

本节用 Pydantic 构建了三个数据模型:

模型用途关键点
Student学生完整信息用于查询与响应
StudentCreate创建学生去掉 id 等系统字段,约束客户端输入
StudentUpdate更新学生所有字段可选,去掉不可改的学号

两个要点:

  1. 请求体变成模型实例后,不能再按字典调用 get() 或 ** 拆包,统一用 model_dump() 转字典。
  2. 更新接口必须加 exclude_unset=True,只更新用户提交的字段,未提交字段保持原值,避免被覆盖成 None。

至此,学生接口的模型校验全部完成。

课程推荐

想系统学习这门课程?

你已阅读完本文。需要更完整的知识体系,可以继续了解「FastAPI 项目实战」。

查看课程详情