API 输出模型怎么设计:从 model_dump() 到显式展示层
工程场景与决策冲突
在业务 API 里直接返回 model.model_dump(),短期看几乎没有成本:领域对象已经校验过,FastAPI 也能把字典编码成 JSON。但领域模型一旦新增内部字段,公开响应会同步膨胀。这种"自动同步"恰好破坏了 API 合同应该具备的稳定性。
我在真实项目里采用的边界是:领域对象保留去重和一致性所需的内部事件键;展示层显式选择公开字段、计算展示值;API 测试再断言内部键不存在。

核心取舍不是要不要使用 Pydantic,而是同一个模型能否同时承担"内部业务状态"和"外部数据合同"两种变化速度不同的职责。
方案拆解与关键权衡
方案一:完整 model_dump()
python
return subscription.model_dump()
优点是快,缺点是默认公开全部可序列化字段。领域层新增字段会隐式修改 API,代码审查时也很难从路由看出响应边界。
方案二:exclude 黑名单
python
return subscription.model_dump(exclude={"private_event_key"})
它能处理已知字段,却无法阻止未来的新字段。对于安全边界,这相当于维护一张持续增长的遗漏清单。
方案三:include 白名单
python
return subscription.model_dump(
include={"article_id", "plan_name", "quantity"}
)
白名单比黑名单稳,但如果许多路由各自维护集合,字段类型、说明、嵌套结构和 OpenAPI 合同仍可能分叉。它适合小型内部接口,不是复杂公开 API 的终点。
方案四:独立响应模型加展示层

这套方案代码最多,但边界最清楚:
- 领域模型按内部一致性演进;
- 展示层负责字段选择和派生值;
- 响应模型负责公开类型与文档;
- 路由只协调调用,不重新拼装隐私规则。
它把"默认拒绝"落实到了类型结构里。
实现链路与最小示例
先定义包含内部字段的领域对象:
python
from pydantic import BaseModel
class SubscriptionItem(BaseModel):
article_id: str
plan_name: str
quantity: int
unit_price_fen: int
private_event_key: str
再定义公开合同:
python
class SubscriptionView(BaseModel):
article_id: str
plan_name: str
quantity: int
amount_yuan: str
显式映射集中在展示函数中:
python
def present(item: SubscriptionItem) -> SubscriptionView:
amount = item.quantity * item.unit_price_fen / 100
return SubscriptionView(
article_id=item.article_id,
plan_name=item.plan_name,
quantity=item.quantity,
amount_yuan=f"{amount:.2f}",
)
FastAPI 路由声明最终合同:
python
@app.get("/subscriptions/{article_id}", response_model=SubscriptionView)
def get_subscription(article_id: str) -> SubscriptionView:
return present(repository.get(article_id))
这里有两层防线。展示函数只构造允许字段,response_model 再负责验证、过滤和 OpenAPI 描述。不要把第二层当作省略第一层的理由:显式展示层还能承载脱敏、格式化和对象级授权判断。
证据、限制与自动化边界
真正能防止回归的不是架构图,而是负向测试:
python
def test_response_omits_internal_event_key(client) -> None:
payload = client.get("/subscriptions/article-1").json()
assert set(payload) == {
"article_id",
"plan_name",
"quantity",
"amount_yuan",
}
assert "private_event_key" not in payload

我更偏向同时保留"键集合完全相等"和"敏感键不存在"两种断言。前者能发现任何合同漂移,后者直接表达安全意图,失败时更容易理解。
这套设计也有边界:
- 如果不同调用方权限不同,不能只做一个全局响应模型,还需要按授权上下文选择展示策略;
- 嵌套模型必须逐层定义公开类型,不能在最内层重新透传领域对象;
- 对超大列表,展示层的转换成本需要测量,但不能以未经测量的性能担忧换取字段泄漏风险;
- 字段已经泄漏时,除了修代码,还要检查缓存、日志和下游存储,并轮换可用凭据。
Pydantic 官方文档说明了序列化的能力;FastAPI 的 response_model 说明了输出过滤和文档合同。工程上的关键推论是:序列化 API 是工具,公开权限仍需要由应用边界决定。
可复用检查清单
- 领域模型与公开响应模型职责分离
- 默认使用允许字段白名单
- 展示层集中处理派生值与脱敏
- 路由声明返回类型和
response_model - 嵌套对象不复用内部模型
- 测试断言响应键集合与敏感字段不存在
- 领域模型新增字段时,公开合同不会自动变化
- 泄漏恢复覆盖缓存、日志、下游副本和凭据轮换
收束
你更倾向在小项目里用 include,还是从第一天就拆独立响应模型?如果有不同权限等级的响应设计,也欢迎分享你的取舍。
发布前门禁
- 已减少重复背景并突出工程选择
- 代码与边界均有项目或官方依据
- 未给出未经验证的数据结论
- 本轮无实验卡,未补写实验结论