FastAPI 响应也要有契约:用 response_model 挡住字段泄漏
摘要:请求体有校验,响应同样需要边界。本文拆分创建模型与读取模型,用 response_model 过滤内部字段,并用 201、404 准确表达接口结果。

图:内部对象经过响应模型过滤,只有契约允许的字段能够离开服务。
很多接口只盯着输入校验,却把数据库对象原样返回。这样做最危险的地方不是“不够优雅”,而是某次加上 password_hash、内部备注或成本字段后,它们可能悄悄出现在公共响应里。
解决办法是把响应也当作契约:业务内部可以拥有更多字段,客户端只能拿到明确声明的那一部分。下面用一个故意带 internal_note 的任务,验证 FastAPI 如何把它挡在响应之外。
先制造一次字段泄漏,再把它挡住
fastapi dev examples/ch04_response_errors/main.py
curl -i http://127.0.0.1:8000/tasks/1
curl -i http://127.0.0.1:8000/tasks/999
第一条预期为 200,且看不到 internal_note;第二条预期为 404:
{"detail":"任务不存在"}
先找到那个不该公开的内部字段
cd 04-fastapi-beginner
source .venv/bin/activate
打开 本文示例代码 和 对应测试。先注意存储字典中有一个不应该公开的 internal_note。
拆开输入模型和输出模型
拆分输入与输出模型
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=100)
description: str | None = Field(default=None, max_length=500)
class TaskRead(TaskCreate):
id: int
completed: bool
客户端创建任务时只提供标题和说明;服务读取任务时额外返回服务生成的 ID 和完成状态。
约束创建响应
@app.post(
"/tasks",
response_model=TaskRead,
status_code=status.HTTP_201_CREATED,
)
def create_task(payload: TaskCreate):
return {
"id": 2,
"completed": False,
"internal_note": "不会返回",
**payload.model_dump(),
}
函数内部返回了额外字段,但客户端只能收到 TaskRead 声明的字段。201 表示服务器成功创建了新资源。
抛出业务错误
task = TASKS.get(task_id)
if task is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="任务不存在",
)
return task
参数 999 本身是合法整数,因此不是 422;只是对应资源不存在,所以返回 404。

图:输入模型负责接收创建数据,输出模型负责筛掉不应公开的内部字段。
同时验证 201、404 和字段过滤
curl -s http://127.0.0.1:8000/tasks/1
curl -i http://127.0.0.1:8000/tasks/999
curl -i -X POST http://127.0.0.1:8000/tasks \
-H "Content-Type: application/json" \
-d '{"title":"定义响应契约"}'
检查三件事:内部字段没有泄漏、不存在返回 404、创建成功返回 201。
自动验证:
python -m pytest tests/test_ch04.py -q

图:状态码说明结果类型,响应模型继续约束最终交给调用者的字段。
response_model 不只是文档
请求模型和响应模型分别位于系统边界两侧:
客户端 JSON -> TaskCreate -> 业务逻辑 -> 内部对象 -> TaskRead -> JSON 响应
response_model 不只是文档,它会对返回值执行验证和序列化。若业务代码缺少 id,这是服务端违反自己的契约,应该在测试中尽早暴露。
常见状态码:200 查询或修改成功,201 创建成功,204 删除成功且没有响应体,404 资源不存在,422 参数校验失败。不要把所有结果都包装成 HTTP 200 再在 JSON 里塞自定义错误码。
状态码和响应边界的常见误区
- 直接返回数据库对象的所有字段:密码哈希、内部备注可能泄漏。
- 找不到任务时返回
None:响应模型校验可能变成 500,应明确抛出 404。 - 手写数字
201到处散落:使用status.HTTP_201_CREATED更易读。 - 把不存在当 422:422 是输入形状或约束不合法,404 是合法标识对应资源不存在。
- 在
detail中暴露堆栈或数据库 SQL:错误体应该对调用者稳定且安全。
把响应契约继续做严
- 为任务增加内部字段
owner_email,证明响应中不会出现。 - 增加
DELETE /tasks/{task_id},成功返回 204,不存在返回 404。 - 故意删除返回值中的
id,观察测试或请求会出现什么。 - 为 404 响应增加精确 JSON 断言。
- 思考:更新接口成功应该返回 200 还是 204?分别适用于什么场景?
最后,用测试固定这次改动
手动请求通过后,运行与本文对应的自动化测试:
python -m pytest tests/test_ch04.py -q
python tests/validate_course.py
写在最后
稳定 API 同时约束输入与输出,并通过 HTTP 状态码准确表达结果。下一篇会把已经掌握的参数、模型和错误组合起来,完成第一个完整 CRUD 闭环。