FastAPI 附加响应(Additional Responses)完全指南:用 `responses` 参数扩展 OpenAPI 状态码、Media Type 与响应 Schema
本文是 FastAPI 高级主题「附加响应(Additional Responses)」的完整实战指南。它围绕每个 path operation 装饰器上的 responses 参数展开:从「为错误码补充 Pydantic 模型与 JSON Schema」到「为同一状态码追加多个 media type」,再到组合 response_model、自定义 description/example 以及用字典解包复用预定义响应。读完本文,你将掌握如何在 FastAPI 自动生成的 OpenAPI 与 Swagger 文档中,精确刻画每个接口可能返回的每一种响应,让客户端代码生成与 API 契约更加准确。文中所有结论均可在本仓库的示例源码、单元测试与核心实现中得到印证。
本指南对应仓库原始文档为 docs/es/docs/advanced/additional-responses.md,示例代码统一收录于 docs_src/additional_responses/。
什么是附加响应,为什么要声明它们
FastAPI 会自动为每个 path operation 生成 OpenAPI 中的响应描述:基于 response_model 生成默认状态码(通常是 200)的响应 Schema,并在请求体需要校验时自动追加 422 Validation Error。但真实世界里的接口远不止「成功」与「参数校验失败」两种结果——你可能还要返回 404 找不到资源、403 无权限、302 重定向,甚至让同一个端点既能返回 JSON 又能返回图片。
附加响应(Additional Responses) 就是用来解决这一问题的:你可以额外声明具有不同状态码、不同 media type、不同描述与不同响应模型的响应。这些附加响应会被纳入生成的 OpenAPI schema,因此也会自动呈现在 /docs(Swagger UI)等交互式文档中。
有一点必须首先明确:这些附加响应声明只负责「文档与契约」,FastAPI 不会替你构造并返回它们。当代码路径真正走到某个附加响应(例如 404)时,你必须在端点内直接返回一个 Response 对象(如 JSONResponse、FileResponse),带上对应的状态码与内容。这一点在示例代码与下文每个小节中都会被反复强调。
用 model 键声明带 Pydantic 模型的附加响应
responses 参数接收一个 dict:
- 键(key) 是响应状态码,例如
404; - 值(value) 是另一个
dict,用于描述该状态码对应的响应信息。
每个响应 dict 中可以放置一个 model 键,值为一个 Pydantic 模型,其作用与 response_model 完全一致:FastAPI 会为该模型生成 JSON Schema,并把它嵌入到 OpenAPI 的正确位置。这样其他应用、SDK 生成器等下游工具就能看到 404 时返回的确定结构。
以下示例声明了一个额外的 404 响应,其响应体是一个 Message 模型:
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel
class Item(BaseModel):
id: str
value: str
class Message(BaseModel):
message: str
app = FastAPI()
@app.get("/items/{item_id}", response_model=Item, responses={404: {"model": Message}})
async def read_item(item_id: str):
if item_id == "foo":
return {"id": "foo", "value": "there goes my hero"}
return JSONResponse(status_code=404, content={"message": "Item not found"})
完整可运行代码见 docs_src/additional_responses/tutorial001_py310.py。
请特别注意函数体中的返回逻辑:
- 命中
item_id == "foo"时返回普通dict,FastAPI 会按response_model=Item序列化并以200返回; - 未命中时,必须直接
return JSONResponse(status_code=404, content={...}),因为404的附加响应声明只影响文档,并不会被自动触发。
model 键不是 OpenAPI 的一部分
需要强调:model 这个键并不属于 OpenAPI 规范,它只是 FastAPI 自定义的便捷写法。FastAPI 的处理流程是:
- 从
responses字典中取出model对应的 Pydantic 模型; - 为它生成 JSON Schema;
- 把 Schema 放到 OpenAPI 中真正的「正确位置」。
从源码可以清晰看到这一解构过程。路由注册阶段,fastapi/routing.py 会遍历 route.responses.items(),取出每个附加响应里的 model,校验该状态码允许存在响应体,然后以 serialization(序列化)模式创建对应的模型字段:
response_fields = {}
for additional_status_code, response in route.responses.items():
assert isinstance(response, dict), "An additional response must be a dict"
model = response.get("model")
if model:
assert is_body_allowed_for_status_code(additional_status_code), (
f"Status code {additional_status_code} must not have a response body"
)
response_name = f"Response_{additional_status_code}_{route.unique_id}"
response_field = create_model_field(
name=response_name, type_=model, mode="serialization"
)
response_fields[additional_status_code] = response_field
而 OpenAPI 文档生成阶段,fastapi/openapi/utils.py 会先对附加响应字典做一次深拷贝并把 model 弹出去(process_response.pop("model", None)),保证该自定义键不会泄漏进最终文档。
Schema 落在 OpenAPI 的哪个位置
「正确位置」的层级是:
- 顶层
responses对象中,以状态码为键的 Response Object; - 其下的
content键,值为一个 JSON 对象; content中以 media type(如application/json)为键的值,又是一个 JSON 对象;- 其中包含
schema键——这里就是 Pydantic 模型 JSON Schema 的落点;- FastAPI 通常不把 Schema 内联到这里,而是在此放入一个指向全局 JSON Schemas 的
$ref引用。这样同一份 Schema 可以在 OpenAPI 的components.schemas中被多处复用,也让下游的代码生成工具能拿到完整的模型定义。
- FastAPI 通常不把 Schema 内联到这里,而是在此放入一个指向全局 JSON Schemas 的
对于上面的例子,生成的 /items/{item_id} 的 OpenAPI 响应部分大致如下:
{
"responses": {
"404": {
"description": "Additional Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Message"
}
}
}
},
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Item"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
同时,全局 components.schemas 中会出现完整的模型定义,Message 与 Item 都只在这里定义一次,别处一律用 $ref 引用:
{
"components": {
"schemas": {
"Message": {
"title": "Message",
"required": [
"message"
],
"type": "object",
"properties": {
"message": {
"title": "Message",
"type": "string"
}
}
},
"Item": {
"title": "Item",
"required": [
"id",
"value"
],
"type": "object",
"properties": {
"id": {
"title": "Id",
"type": "string"
},
"value": {
"title": "Value",
"type": "string"
}
}
},
"ValidationError": {
"title": "ValidationError",
"required": [
"loc",
"msg",
"type"
],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {
"type": "string"
}
},
"msg": {
"title": "Message",
"type": "string"
},
"type": {
"title": "Error Type",
"type": "string"
}
}
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {
"$ref": "#/components/schemas/ValidationError"
}
}
}
}
}
}
}
值得一提的是,当前仓库的测试快照表明,基于 Pydantic v2 实际生成的 ValidationError 结构会比上面示意更细:loc 的元素是 string 或 integer 的联合类型,并附带 input、ctx 等字段。完整断言可查看 tests/test_tutorial/test_additional_responses/test_tutorial001.py 中的 test_openapi_schema,该测试同时验证了 /items/foo 返回 200 正常对象、/items/bar 返回 404 错误体——即「附加响应需手动 JSONResponse 返回」这一约定的可运行证明。
为主响应补充多种 Media Type
同一个 responses 参数还能用来给同一个主状态码追加不同的 media type。例如一个端点大多数时候返回 JSON 对象(media type application/json),但在特定条件下返回一张 PNG 图片。可以这样声明:
from fastapi import FastAPI
from fastapi.responses import FileResponse
from pydantic import BaseModel
class Item(BaseModel):
id: str
value: str
app = FastAPI()
@app.get(
"/items/{item_id}",
response_model=Item,
responses={
200: {
"content": {"image/png": {}},
"description": "Return the JSON item or an image.",
}
},
)
async def read_item(item_id: str, img: bool | None = None):
if img:
return FileResponse("image.png", media_type="image/png")
else:
return {"id": "foo", "value": "there goes my hero"}
完整代码见 docs_src/additional_responses/tutorial002_py310.py。
这里的关键点:
- 在
responses[200]里我们没有提供model,而是直接写入 OpenAPI 原生结构content.image/png(空对象即可,图片没有 JSON Schema 可言); - 实际返回图片的代码路径中,必须直接返回
FileResponse("image.png", media_type="image/png"),才能让响应头真正带上image/png; - 由于
response_model=Item依然生效,FastAPI 会为200补上application/json分支的 Schema,最终200下同时存在image/png与application/json两个 media type,交给客户端根据能力自行选择。
Media Type 的推断规则
除非你在 responses 中为某个响应显式指定了不同的 media type,否则 FastAPI 会假设该响应沿用主响应类(response class)的 media type——默认即 application/json。
这一行为与源码高度一致:在 fastapi/openapi/utils.py 中,当某个附加响应带模型字段时,会执行:
media_type = route_response_media_type or "application/json"
也就是说:
- 如果主响应类有明确的 media type(如默认的
JSONResponse对应application/json),附加响应的content会落到该 media type 下; - 如果你自定义了响应类且其 media type 为
None,那么凡带模型的附加响应一律回退到application/json。
了解这一规则,可以避免在 Swagger 文档里看到意料之外的 media type 键。
组合 response_model、status_code 与 responses
附加响应声明并非只能单独使用。你完全可以把 response_model、status_code 与 responses 三个维度的信息组合起来:
- 先用
response_model声明主响应的模型(默认200,也可用status_code改为主状态码); - 再在
responses中为这一主响应补充额外信息——这些信息会被 FastAPI 保留并直接合并进 OpenAPI,与模型的 JSON Schema 并存,互不覆盖。
下面的例子展示了两种典型场景:一个 404 响应既使用 Pydantic 模型又带自定义 description;一个 200 响应沿用 response_model 的 Schema,同时注入自定义 example:
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel
class Item(BaseModel):
id: str
value: str
class Message(BaseModel):
message: str
app = FastAPI()
@app.get(
"/items/{item_id}",
response_model=Item,
responses={
404: {"model": Message, "description": "The item was not found"},
200: {
"description": "Item requested by ID",
"content": {
"application/json": {
"example": {"id": "bar", "value": "The bar tenders"}
}
},
},
},
)
async def read_item(item_id: str):
if item_id == "foo":
return {"id": "foo", "value": "there goes my hero"}
else:
return JSONResponse(status_code=404, content={"message": "Item not found"})
完整代码见 docs_src/additional_responses/tutorial003_py310.py。
从生成的 OpenAPI 可以看到合并细节(对应测试快照见 tests/test_tutorial/test_additional_responses/test_tutorial003.py):
404:description变成我们指定的"The item was not found",并因model: Message自动带上$ref: #/components/schemas/Message的content;200:description变为"Item requested by ID";content.application/json.schema依然是自动生成的$ref: #/components/schemas/Item,而我们在responses中提供的example被合并进同一个content.application/json对象里——Schema 与示例互不冲突;422:校验失败响应依然自动存在,不需要你手动声明。
这一切都归结于 fastapi/openapi/utils.py 的合并实现:FastAPI 先把主响应的模型 Schema 写进 content(通过 setdefault 等幂等操作),再对 responses 里用户提供的信息做深拷贝、剔除 model 后,通过 deep_dict_update 把自定义的 description、content、example 等字段合并上去,从而既保留自动 Schema 又不丢用户信息。
最终,所有信息都会汇入 OpenAPI 并显示在 API 文档中。运行该应用后打开 /docs,你会看到 GET /items/{item_id} 的 Responses 区域同时列出 200 与 404,各自携带不同的描述与示例:
复用预定义响应:字典解包技巧
当项目中存在大量 path operations 都需要携带同一组「预定义响应」(例如统一的 404、302、403 描述)时,逐条复制会非常啰嗦。Python 的字典解包语法 **dict_to_unpack 正好可以派上用场:
old_dict = {
"old key": "old value",
"second old key": "second old value",
}
new_dict = {**old_dict, "new key": "new value"}
解包后,new_dict 会同时包含 old_dict 的全部键值对以及新增的键值对:
{
"old key": "old value",
"second old key": "second old value",
"new key": "new value",
}
应用到 FastAPI 上,就是先定义一个可复用的 responses 字典,再在每个 path operation 里把它与自定义响应合并:
from fastapi import FastAPI
from fastapi.responses import FileResponse
from pydantic import BaseModel
class Item(BaseModel):
id: str
value: str
responses = {
404: {"description": "Item not found"},
302: {"description": "The item was moved"},
403: {"description": "Not enough privileges"},
}
app = FastAPI()
@app.get(
"/items/{item_id}",
response_model=Item,
responses={**responses, 200: {"content": {"image/png": {}}}},
)
async def read_item(item_id: str, img: bool | None = None):
if img:
return FileResponse("image.png", media_type="image/png")
else:
return {"id": "foo", "value": "there goes my hero"}
完整代码见 docs_src/additional_responses/tutorial004_py310.py。
这段代码的效果是:每个需要这些响应的端点,只要写一行 responses={**responses, ...},即可继承预定义的 404/302/403,再叠加本端点特有的响应(这里是给 200 增加 image/png)。由 tests/test_tutorial/test_additional_responses/test_tutorial004.py 的 OpenAPI 快照可以看到最终结果:404、302、403 各自带有预定义的描述;200 同时存在 image/png 与带 Item Schema 的 application/json 两个 media type;422 依然自动保留。测试中还通过 client.get("/items/foo?img=1") 验证了返回图片时 Content-Type 确实是 image/png。
值得补充的是,这一「合并语义」在仓库更深处也存在呼应:路由处理时父级与子级路由、include_router 携带的默认 responses 与当前路由的 responses 都会做字典合并,例如 fastapi/routing.py 中的 responses={**parent_router.responses, **(responses or {})},以及 fastapi/routing.py 中的 combined_responses = {**self.responses, **responses}——可见字典解包是 FastAPI 内部贯穿始终的默认响应合并模式。
关于 description 默认值与 OpenAPI 规范速查
最后一个实用细节:当你没有为附加响应提供 description 时,FastAPI 会尝试自动填充标准状态码文本(如 404 → "Not Found")。从 fastapi/openapi/utils.py 的实现看,它先从 FastAPI 内部的状态码区间表(status_code_ranges,覆盖 1xx~5xx 等信息)取值,取不到时再回退到 Python 标准库 http.client.responses。你在前面 OpenAPI 输出中看到的 "Successful Response"、"Validation Error" 等描述,正是这些自动规则与默认生成逻辑共同作用的结果——而一旦你在 responses 字典里手动给出 description,它就会像 "The item was not found" 那样覆盖默认值。
若要了解 responses 参数中每个值到底还能写什么,建议阅读 OpenAPI 3.1.0 规范中的两个对象定义:
- OpenAPI Responses Object:定义响应对象集合的容器结构,也是
responses参数的顶层对应物; - OpenAPI Response Object:其内可写字段包括
description、headers、content(在这里声明不同 media type 及其 JSON Schema)、links等——responses参数中每个状态码的 value 字典,理论上都能直接放进这些字段。
需要注意的是,附加响应声明中 model 键是 FastAPI 特有的便捷入口,而 description、headers、content、links 等都是合法的 OpenAPI 原生字段,FastAPI 会原样保留并合并进最终文档。
小结与建议
围绕一个 path operation,FastAPI 通过 responses 参数提供了完备的响应契约能力:
| 能力 | 用法 | 对应示例 |
|---|---|---|
| 为额外状态码声明响应模型 | responses={404: {"model": Message}} |
tutorial001_py310.py |
| 为同一状态码追加 media type | responses={200: {"content": {"image/png": {}}}} |
tutorial002_py310.py |
| 组合模型与自定义描述/示例 | responses={404: {"model": Message, "description": ...}, 200: {...}} |
tutorial003_py310.py |
| 复用预定义响应集合 | responses={**shared, 200: {...}} |
tutorial004_py310.py |
实际落地时请始终记住两条铁律:其一,附加响应只是 OpenAPI 声明,真正返回时必须在端点内直接构造并返回对应的 Response 子类(JSONResponse、FileResponse 等);其二,model 是 FastAPI 扩展键,其余字段遵循 OpenAPI 规范。这两条约束分别由 fastapi/routing.py 的字段预构建和 fastapi/openapi/utils.py 的文档合并逻辑保障,仓库内 tests/test_tutorial/test_additional_responses/ 下的四个测试文件则为每一份示例的运行时行为与最终 OpenAPI 输出提供了可重复的验证基准。建议你在自己的项目中用 /openapi.json 反复核对生成的契约,确保文档与真实返回保持一致。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
