FastAPI 声明 OpenAPI 额外响应:responses 参数、model 模型与多媒体类型实践
本文讲解如何借助路径操作装饰器的 responses 参数,在 FastAPI 中为 OpenAPI 文档声明额外的状态码、媒体类型与描述信息,并深入到 fastapi/routing.py 与 fastapi/openapi/utils.py 的源码层,还原 model 键如何被解析成 JSON Schema、与 response_model 自动生成的内容如何合并,以及预定义响应如何通过字典解包在各接口间复用。
需要先说明:这是一个偏进阶的主题。如果你刚开始使用 FastAPI,通常暂时用不到这些能力。
为什么需要额外响应
默认情况下,FastAPI 只根据路径操作的 status_code 和 response_model 生成一条主响应。但真实 API 往往还需要向调用方声明:
- 其他可能的状态码(如
404、403、302)及其响应结构; - 同一响应可返回的多种媒体类型(如 JSON 或 PNG 图片);
- 自定义的描述、示例(
example)、头(headers)、链接(links)等 OpenAPIResponse Object字段。
这些额外响应(additional responses)会被完整写入 OpenAPI 文档,因此也会自动出现在 Swagger UI / ReDoc 等 API 文档界面中。
一个关键约束:对于额外响应,你必须直接返回一个 Response 对象(如 JSONResponse、FileResponse),并自行带上状态码和内容。FastAPI 在运行期不会对额外响应做校验或序列化,responses 参数仅服务于文档生成。
从源码看,这个约束的起点在 fastapi/routing.py:route.responses = responses or {},装饰器传入的字典被原样挂在路由对象上,等待 OpenAPI 生成阶段处理。
带 model 的额外响应
你可以在 路径操作装饰器 上传入参数 responses。它接收一个 dict:键是每个响应的状态码(如 200、404),值是包含该响应信息的另一个 dict。
每个响应 dict 都可以包含一个 model 键,其值是一个 Pydantic 模型——用法与 response_model 一致。FastAPI 会取出这个模型,生成其 JSON Schema,并放入 OpenAPI 的正确位置。
例如,声明一个状态码为 404、结构为 Pydantic 模型 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。
两点注意事项:
- 你必须直接返回
JSONResponse(连同状态码与内容)。 model键不是 OpenAPI 规范的一部分。FastAPI 只是从那里取出 Pydantic 模型、生成 JSON Schema,再把它放到正确的位置。
Schema 被放入的"正确位置"是:
content键,其值是一个 JSON 对象(dict),其中包含:- 一个媒体类型键,例如
application/json,其值又是一个 JSON 对象,其中包含:schema键,其值即为该模型的 JSON Schema——这里就是正确位置。- FastAPI 在此处不会直接内联 Schema,而是添加一个指向 OpenAPI 其他位置全局 JSON Schema 的引用(
$ref)。这样其他应用和客户端可以直接使用这些 JSON Schema,提供更好地代码生成工具等。
- FastAPI 在此处不会直接内联 Schema,而是添加一个指向 OpenAPI 其他位置全局 JSON Schema 的引用(
- 一个媒体类型键,例如
源码层: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这类无体状态码不能声明model);模型字段以serialization模式创建,与响应序列化行为保持一致。
第二段:OpenAPI 生成时写入 Schema,位于 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"
...
field = route.response_fields.get(additional_status_code)
additional_field_schema: dict[str, Any] | None = None
if field:
additional_field_schema = get_schema_from_model_field(
field=field,
model_name_map=model_name_map,
...
)
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)
可以看到:model 键在这里被显式 pop 掉(印证了它不属于 OpenAPI);模型经 get_schema_from_model_field 转为 Schema 后写入 content -> 媒体类型 -> schema 路径,与上文描述的位置完全一致;若你没有显式指定媒体类型,则回退为 route_response_media_type or "application/json"。
此外还有一个源码级细节:str(additional_status_code).upper() 且 DEFAULT 会被转成小写 default——也就是说你可以用 default(或 DEFAULT)作为键,声明一个匹配所有未列出状态码的兜底响应。
生成的 OpenAPI 输出
该路径操作最终生成的 responses 如下(基于当前仓库测试快照 tests/test_tutorial/test_additional_responses/test_tutorial001.py):
{
"responses": {
"404": {
"description": "Not Found",
"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 的描述 "Not Found" 来自源码中的描述回退链(fastapi/openapi/utils.py):先取你在 responses 中写的 description,再取主响应已有的描述,然后查状态码区间与标准 HTTP 状态码文本(http.client.responses),都没有才兜底为 "Additional Response"。
422 是 FastAPI 自动附加的:当路由存在路径参数或请求体、且你没有声明 422/4XX/default 时,会默认加入校验错误响应(见 fastapi/openapi/utils.py)。
Schema 本体则被引用到 OpenAPI 的另一处 components 中:
{
"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"
}
}
}
}
}
}
}
测试如何验证这一行为
tests/test_tutorial/test_additional_responses/test_tutorial001.py 同时验证了运行时与文档两侧:
def test_path_operation_not_found():
response = client.get("/items/bar")
assert response.status_code == 404, response.text
assert response.json() == {"message": "Item not found"}
运行时请求确实返回 404 与 {"message": "Item not found"}(由端点直接返回的 JSONResponse 决定),test_openapi_schema 则对整个 /openapi.json 做快照断言,锁定 $ref 与 components 结构。
为主响应声明多种媒体类型
同一个 responses 参数还可以用于为同一条主响应添加不同的媒体类型。例如,声明你的 路径操作 既能返回 JSON 对象(媒体类型 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。
两点注意事项:
- 图片必须用
FileResponse直接返回; - 媒体类型的默认规则:除非你在
responses参数中显式指定了其他媒体类型,FastAPI 会假定该响应与主响应类拥有相同的媒体类型(默认是application/json)。但若你指定了一个媒体类型为None的自定义响应类,FastAPI 会为任何带关联模型的额外响应使用application/json。
第二条规则正对应源码中的 media_type = route_response_media_type or "application/json"(fastapi/openapi/utils.py):主响应类无媒体类型时,带 model 的额外响应会统一落到 application/json 下。
合并多来源信息: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:
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,并显示在 API 文档中:
合并机制在源码中对应 deep_dict_update(fastapi/openapi/utils.py):FastAPI 先把主响应(response_model 生成的 Schema)写入 operation["responses"],再用你 responses 中的内容做深合并——用户提供的键优先,未提供的键保留自动生成值。描述字段的优先级链为:responses 中显式写的 description → 主响应已有的 description → 标准状态码文本 → "Additional Response"(fastapi/openapi/utils.py)。
复用预定义响应,并与自定义响应合并
你可能希望有一组适用于很多 路径操作 的预定义响应,同时又想为每个 路径操作 补充各自需要的自定义响应。
此时可以使用 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",
}
用这个技术,你可以在 路径操作 中复用一组预定义响应,并与额外的自定义响应组合。例如:
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 中预定义 404、302、403 三个响应,在装饰器里解包后与 200 的图片媒体类型声明合并,最终该路径操作的文档将同时展示这四种响应。对应的断言见 tests/test_tutorial/test_additional_responses/test_tutorial004.py。
responses 中可以写入哪些 OpenAPI 字段
要查看响应中究竟可以包含什么,可以查阅 OpenAPI 3.1 规范中的两个对象(此处不附外部链接,规范名称如下即可检索):
- Responses Object:响应对象集合,包含其中的
Response Object; - Response Object:其中的任何字段都可以直接放进
responses参数的每一条响应里,包括description、headers、content(在其中声明不同的媒体类型与 JSON Schema)、以及links。
结合源码行为,使用时需注意:
- 每个状态码对应的值必须是
dict(fastapi/routing.py 的断言); model是 FastAPI 专用扩展键,不属于 OpenAPI,生成文档时会被移除,且要求对应状态码允许携带响应体;- 键会被统一转为字符串并大写化,
DEFAULT会归一化为 OpenAPI 的default兜底键(fastapi/openapi/utils.py); - 你写入的
description、headers、content、links等标准字段会与自动生成的主响应做深合并,同名键以你提供的为准。
小结
responses参数让你在不改变运行行为的前提下,为 OpenAPI 文档补充任意状态码、媒体类型、描述、示例与链接;model键把 Pydantic 模型接进文档生成链路:路由注册期创建序列化字段,OpenAPI 生成期写入content -> 媒体类型 -> schema,并以$ref指向全局components/schemas;- 运行期你必须自己返回
JSONResponse、FileResponse等Response对象; - 借助
deep_dict_update的深合并与**dict解包,预定义响应和response_model、status_code的信息可以无缝组合,形成完整、可直接驱动客户端代码生成的 API 文档。
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 StartedRust0623
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
