FastAPI 进阶实战:用 `responses` 参数声明额外响应,自定义 OpenAPI 的 responses 结构
本文基于 FastAPI 官方文档 Advanced 章节的 Additional Responses 主题,讲解如何通过 path operation decorator 的 responses 参数为 API 声明额外状态码、额外媒体类型与自定义描述,并完整呈现生成的 OpenAPI 结构。读完本文,你将掌握 model 键的用法、多种响应信息来源(response_model、status_code、responses)的合并规则,以及基于源码的底层实现原理,能够写出可被 API 文档与代码生成工具直接消费的完整响应契约。
一、为什么需要声明额外响应
这是一个相对进阶的话题。如果你是刚开始使用 FastAPI,短期内可能用不到它;但当你需要让 OpenAPI Schema(以及交互式 API 文档)如实描述端点所有可能的响应——而不只是成功的那一个——时,它就是标准工具。
responses 参数允许你声明额外的响应:带额外状态码、媒体类型、描述等信息。这些额外响应会被写入 OpenAPI Schema,从而出现在 API 文档中。
它的取值是一个 dict:
responses: dict[int | str, dict[str, Any]] | None = None
- 键是响应的状态码(如
404、302),可以是int也可以是str; - 值是对应响应的
dict,结构遵循 OpenAPI 的 Response Object(可含description、headers、content、links等键),FastAPI 额外支持一个非 OpenAPI 的model键。
一个必须牢记的前提:对这些额外响应,你需要直接返回一个 Response 对象(例如 JSONResponse、FileResponse),并自行设置状态码和内容。responses 参数只负责"声明契约",FastAPI 不会替你拦截或自动生成这些响应。
二、带 model 键的额外响应
在 path operation decorator 中传入 responses 参数,每个响应的 dict 里可以有一个 model 键,其值是 Pydantic 模型,用法与 response_model 完全一致。FastAPI 会拿到该模型,生成其 JSON Schema,并放到 OpenAPI 的正确位置。
例如,为 /items/{item_id} 声明一个状态码 404、使用 Pydantic 模型 Message 的额外响应(完整文件见 tutorial001_py310.py):
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"})
注意两处细节:
- 必须直接返回
JSONResponse——model只影响 OpenAPI 声明,运行时行为由你返回的Response决定; model键不属于 OpenAPI。FastAPI 会从这里取出 Pydantic 模型,生成 JSON Schema 后放在正确位置:content键,其值是另一个 JSON 对象(dict),包含:- 媒体类型键(例如
application/json),其值又是一个 JSON 对象,包含:schema键,其值就是模型的 JSON Schema——这就是正确的位置。- FastAPI 不会直接内联该 Schema,而是放一个指向 OpenAPI 全局 Schemas(
components/schemas)的引用($ref)。这样其他应用和客户端可以直接复用这些 JSON Schema,获得更好的代码生成工具支持等。
- FastAPI 不会直接内联该 Schema,而是放一个指向 OpenAPI 全局 Schemas(
- 媒体类型键(例如
源码视角:model 键如何被处理
model 键的处理分为两个阶段,可以从源码中逐一印证。
阶段一:路由注册时提取模型(fastapi/routing.py):
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
这里有三点值得注意:
- 每个响应的值必须是
dict,否则直接断言失败; - 如果
model对应的状态码是不允许响应体的(如204、304、304系列),会抛出断言错误——即model只能配在允许响应体的状态码上; - 提取出的模型被创建为序列化模式的
ModelField,存入route.response_fields,供 OpenAPI 生成阶段使用。
阶段二:OpenAPI 生成时合并(fastapi/openapi/utils.py):
if route.responses:
operation_responses = operation.setdefault("responses", {})
for (
additional_status_code,
additional_response,
) in route.responses.items():
process_response = copy.deepcopy(additional_response)
process_response.pop("model", None)
status_code_key = str(additional_status_code).upper()
if status_code_key == "DEFAULT":
status_code_key = "default"
openapi_response = operation_responses.setdefault(
status_code_key, {}
)
...
field = route.response_fields.get(additional_status_code)
if field:
additional_field_schema = get_schema_from_model_field(field=field, ...)
media_type = route_response_media_type or "application/json"
additional_schema = (
process_response.setdefault("content", {})
.setdefault(media_type, {})
.setdefault("schema", {})
)
deep_dict_update(additional_schema, additional_field_schema)
description = (
process_response.get("description")
or openapi_response.get("description")
or status_text
or "Additional Response"
)
deep_dict_update(openapi_response, process_response)
openapi_response["description"] = description
从源码结构看,可以确认几个行为细节:
model键在生成 Schema 后被pop掉,不会进入最终 OpenAPI 输出;- 状态码键会被转为大写字符串,特殊值
DEFAULT会被转成 OpenAPI 的default键; - 媒体类型推断规则:
media_type = route_response_media_type or "application/json"——若路由指定了带媒体类型的自定义响应类,则沿用之;否则(例如自定义响应类的媒体类型是None)默认使用application/json; - 描述回退链:你在
responses中显式写的description→ 主响应已有的description(如 "Successful Response")→ HTTP 标准状态短语 → 兜底的"Additional Response"; - 最终通过
deep_dict_update做深度合并,所以responses里写的description、example、headers等信息会与主响应的 Schema 信息共存于同一个响应对象中,而不是互相覆盖。
三、生成的 OpenAPI 结构详解
以第二节的示例端点为例,FastAPI 生成的 responses 部分为:
{
"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"
}
}
}
}
}
}
其中:
404是你在responses中声明的,因为没写description,走了回退链的兜底值"Additional Response";200来自response_model=Item;422是 FastAPI 自动追加的验证错误响应——从 fastapi/openapi/utils.py 可以看到,只有当路由有参数或请求体、且responses中尚未存在422、4XX或default任一键时,才会注入该默认项。相关测试见 test_additional_responses_default_validationerror.py。
对应的 JSON Schema 被放到 OpenAPI 的 components.schemas 区域(全局共享):
{
"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"
}
}
}
}
}
}
}
这印证了前文的结论:Message 模型与 Item 一样,以 $ref 的形式被提升到全局 Schema 区,供客户端与代码生成工具直接引用。
四、为主响应添加额外媒体类型
同一个 responses 参数也可以用来为同一个主响应声明多种媒体类型。例如声明 path operation 既可以返回 JSON 对象(application/json),也可以返回 PNG 图片(image/png)(完整文件见 tutorial002_py310.py):
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"}
这里有两条注意点:
- 图片必须用
FileResponse直接返回——与JSONResponse同理,额外媒体类型下的真实响应完全由你返回的Response决定; - 媒体类型的默认推断:除非你在
responses中显式指定了不同的媒体类型,FastAPI 会假设额外响应与主响应类使用相同媒体类型(默认application/json);但如果你指定了一个媒体类型为None的自定义响应类,FastAPI 会对任何带有关联model的额外响应使用application/json。这与源码中media_type = route_response_media_type or "application/json"的逻辑一一对应(见 fastapi/openapi/utils.py)。
上例中 200 响应的 content 会同时包含 application/json(来自 response_model=Item)与 image/png 两个媒体类型键,description 则被你的自定义文案替换为 "Return the JSON item or an image."。
五、组合多来源信息:response_model + status_code + responses
你可以把来自多个地方的响应信息组合起来,包括 response_model、status_code 与 responses 参数。例如:先声明 response_model(使用默认 200 状态码,或按需自定义),然后在 responses 中为同一个响应直接补充 OpenAPI 层面的额外信息。
FastAPI 会保留 responses 中的额外信息,并与模型的 JSON Schema 合并。例如:声明一个状态码 404、使用 Pydantic 模型且带自定义 description 的响应;同时声明一个状态码 200、使用 response_model 但包含自定义 example 的响应(完整文件见 tutorial003_py310.py):
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"})
这些信息会被全部合并进 OpenAPI 并展示在 API 文档中(即文首配图所示效果):
404:description显示为"The item was not found",content下挂Message模型的$ref;200:description显示为"Item requested by ID",同时保留Item模型的 Schema 引用,并额外带上你提供的example对象——这正体现了前文提到的deep_dict_update深度合并:example与自动生成的schema在同一个媒体类型对象内并存,互不冲突。
六、复用预定义响应:dict 解包与 Router 级 responses
如果你有若干预定义响应需要应用到多个 path operation 上,同时每个端点又要叠加各自的自定义响应,可以使用 Python 的 dict 解包技巧 **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",
}
把这个技巧用于 path operation,即可复用预定义响应并叠加自定义响应(完整文件见 tutorial004_py310.py):
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"}
这里顶层的 responses 字典集中定义了 404/302/403 三个共享响应,每个端点在装饰器中解包它,再按需追加自己的 200 媒体类型声明。
更进一步,FastAPI 在 Router 层面原生支持同样的合并机制,甚至不需要手写解包。从源码结构看(fastapi/routing.py):
responses = responses or {}
combined_responses = {**self.responses, **responses}
APIRouter构造时就可以接收responses参数,作为该路由下所有端点的默认额外响应;include_router时,被包含路由的responses会与父级路由的responses按{**self.responses, **responses}顺序合并,子路由的同状态码声明覆盖父级;- 路由与 Router 的响应最终在构建
APIRoute时按{**include_context.responses, **route.responses}合并(见 fastapi/routing.py)。
也就是说,"预定义响应"既可以像上例那样在模块级 dict 中手动解包复用,也可以直接挂在 APIRouter(prefix=..., responses=...) 上由框架自动合并——后者在大型应用中更结构化。
七、responses 值中可以放什么:OpenAPI 规范速查
responses 中每个响应的值,就是 OpenAPI 3.1 规范中的 Response Object(而整个 responses 对应 Responses Object)。按照规范,你可以直接在这些 dict 中包含:
description:响应描述;headers:额外响应头声明;content:按媒体类型区分的响应内容,其中声明不同媒体类型及其 JSON Schema;links:基于响应内容链接到其他操作。
此外,键(状态码)除了具体数值外,还可以使用范围键(如 4XX、5XX)或 default 键来兜底,FastAPI 在生成 OpenAPI 时对 default 做了大小写归一化处理(见 fastapi/openapi/utils.py)。
八、实践要点清单
综合文档与源码,使用 responses 参数时请牢记:
- 运行时契约由你负责:为额外响应返回的内容,必须直接返回
JSONResponse、FileResponse等Response对象,并自行设置状态码与内容;responses参数只影响 OpenAPI 声明与 API 文档展示; model是 FastAPI 扩展键,不属于 OpenAPI:它会被提取、生成 JSON Schema 后从输出中移除;且只能搭配允许响应体的状态码使用,否则路由注册时会断言失败;- 信息是深度合并而非覆盖:
responses中为同一状态码写的description、example、headers会与response_model/status_code生成的 Schema 信息合并到同一个 OpenAPI 响应对象中; 422自动注入且可抑制:只要路由存在参数或请求体,且你没有声明422、4XX或default任一键,FastAPI 会自动追加验证错误响应;声明其中任一键即可接管这一默认行为;- 媒体类型有默认值:未显式指定时,额外响应沿用主响应类的媒体类型;主响应类媒体类型为
None且额外响应带model时,使用application/json; - 描述有回退链:自定义
description→ 主响应描述 → HTTP 标准状态短语 →"Additional Response",因此不写description也不会缺少必填字段。
相关测试覆盖了默认 422 注入、自定义模型、Router 级响应等多种组合场景,可作为行为基准参考,目录见 tests/test_additional_responses_default_validationerror.py、tests/test_additional_responses_custom_model_in_callback.py、tests/test_additional_responses_router.py;教程源码集中于 docs_src/additional_responses/,本文对应的英文原文档为 docs/en/docs/advanced/additional-responses.md。
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 StartedRust0627
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
