6.1 正确理解响应校验与序列化
正确理解响应校验与序列化
路由函数返回的 Python 值、FastAPI 使用的响应模型和客户端收到的 JSON 是三个相连但不相同的边界。返回一个字典,不代表字典里的每个键都会被公开;返回一个 Pydantic 对象,也不代表它可以绕过响应契约;直接返回 Response,则意味着你开始自己负责响应内容。
本节沿用文章管理案例,用一个带内部密码摘要的用户记录对照三种写法:用返回类型注解声明公开模型、用 response_model 过滤字典或对象,以及直接返回 JSONResponse。示例还故意制造一次缺少字段的响应,观察服务端响应校验失败。敏感字段不会出现在前两种正常接口中,直接响应的敏感字段则是为了明确展示绕过过滤的风险。
返回类型注解和 response_model
如果路由函数有返回类型注解,FastAPI 可以把它作为响应模型,用来生成 OpenAPI、校验返回值、序列化并过滤字段。response_model 是路径操作装饰器的参数;当两者同时存在时,FastAPI 以 response_model 为准。用字典、数据库对象等类型作为内部返回值时,通常给 response_model 明确写出对外模型,并把函数返回注解写成真实的内部类型或 Any,让静态工具和运行时职责都清楚。
下面的 UserRecord 继承公开模型只是为了让“返回类型注解”示例保持类型兼容。它包含 password_hash,但 /by-annotation 的公开返回类型是 UserPublic;/by-response-model 返回字典,也由 response_model=UserPublic 负责过滤。
<!-- file: ch06_response/response_contract_demo.py -->
from __future__ import annotations
from typing import Any
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from pydantic import BaseModel
class UserPublic(BaseModel):
id: int
username: str
class UserRecord(UserPublic):
password_hash: str
USER = UserRecord(
id=7,
username="alice",
password_hash="argon2$internal-only",
)
app = FastAPI(title="响应校验与序列化示例")
@app.get("/by-annotation")
def by_annotation() -> UserPublic:
"""返回内部对象,但公开契约由返回类型 UserPublic 声明。"""
return USER
@app.get("/by-response-model", response_model=UserPublic)
def by_response_model() -> Any:
"""返回字典,response_model 负责文档、校验和字段过滤。"""
return USER.model_dump()
@app.get("/direct", response_model=None)
def direct_response() -> JSONResponse:
"""直接响应不会经过 response_model;这里只为演示风险。"""
return JSONResponse(content=USER.model_dump(mode="json"))
@app.get("/broken", response_model=UserPublic)
def broken_response() -> dict[str, Any]:
"""故意缺少 username,模拟服务端违反响应契约。"""
return {"id": USER.id}
def self_check() -> None:
with TestClient(app, raise_server_exceptions=False) as client:
by_annotation = client.get("/by-annotation")
by_response_model = client.get("/by-response-model")
direct = client.get("/direct")
broken = client.get("/broken")
expected_public = {"id": 7, "username": "alice"}
assert by_annotation.status_code == 200
assert by_annotation.json() == expected_public
assert "password_hash" not in by_annotation.json()
assert by_response_model.status_code == 200
assert by_response_model.json() == expected_public
assert "password_hash" not in by_response_model.json()
# 这是刻意展示的危险路径:直接 JSONResponse 自己携带了敏感字段。
assert direct.status_code == 200
assert direct.json()["password_hash"] == "argon2$internal-only"
# 缺少 response_model 要求的字段是服务端错误,而不是正常的空响应。
assert broken.status_code == 500
assert "username" not in broken.text
schema = app.openapi()
for path in ("/by-annotation", "/by-response-model"):
response_schema = schema["paths"][path]["get"]["responses"]["200"]
assert response_schema["content"]["application/json"]["schema"][
"$ref"
].endswith("/UserPublic")
direct_schema = schema["paths"]["/direct"]["get"]["responses"]["200"][
"content"
]["application/json"]["schema"]
assert "$ref" not in direct_schema
print("response_contract_demo self-check passed")
if __name__ == "__main__":
self_check()
在保存文件的目录运行:
python ch06_response/response_contract_demo.py
预期输出:
response_contract_demo self-check passed
最终 JSON 由谁负责
对于 /by-annotation,函数返回的是 UserRecord 对象,但返回类型是 UserPublic,因此文档和响应边界只包含 id、username。对于 /by-response-model,函数返回带有 password_hash 的字典,FastAPI 仍然按照 UserPublic 验证、序列化和过滤。两条接口最终收到的 JSON 相同。
/direct 返回的是已经构造好的 JSONResponse,并显式设置了 response_model=None。这条路径跳过了本节前两种接口依赖的响应模型过滤,因此它把 password_hash 原样带给客户端。生产接口直接返回 Response 时,必须在构造响应前自己选择公开字段;不要把“直接返回更快”当成绕过契约的理由。
/broken 返回缺少 username 的字典。TestClient 使用 raise_server_exceptions=False,所以自检能看到 500 响应;实际服务还应在日志和监控中记录响应校验错误。这个错误说明服务端实现或数据映射违反了已声明契约,不能通过把响应模型删掉来掩盖问题。
FastAPI 当前官方文档将 response_model 的作用概括为生成文档、校验、序列化和过滤输出;也支持用返回类型注解表达模型。不要根据旧文章中某个 validator 日志出现几次,就推断完整模型一定被固定重建两次,或由此宣称有固定性能损失。验证阶段、Pydantic/FastAPI 版本、返回值类型和测试入口都会影响观察结果;若性能确实成为问题,应固定版本做基准并分析真实热点,同时保留对外响应契约。
过滤不是敏感数据治理的唯一层
响应模型是重要的最后一道边界,但查询层仍应尽量只选择需要公开的列。密码摘要、访问令牌、内部备注等字段不应因为“最后会被过滤”就到处流转;它们还可能出现在日志、异常、调试接口或直接响应中。
本节只用内存对象验证字段边界,没有验证数据库 ORM 对象、别名字段或自定义序列化器。接入数据库时,应额外检查 ORM 查询结果到 UserPublic 的映射,并为敏感字段添加失败测试。
资料来源
- 主要参考:fastapi-best-practices 中文 README 的“FastAPI 响应序列化”主题。本节保留响应模型与输出过滤的议题,但不沿用原文关于固定模型创建次数的判断。
- 官方文档:FastAPI 响应模型、直接返回 Response。官方文档说明
response_model参与文档、校验、序列化和过滤,并说明直接返回Response时需要自行负责内容。