首页
/ FastAPI 附加响应(Additional Responses)完全指南:用 `responses` 参数扩展 OpenAPI 状态码、Media Type 与响应 Schema

FastAPI 附加响应(Additional Responses)完全指南:用 `responses` 参数扩展 OpenAPI 状态码、Media Type 与响应 Schema

2026-09-06 19:05:48作者:卓炯娓

本文是 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 对象(如 JSONResponseFileResponse),带上对应的状态码与内容。这一点在示例代码与下文每个小节中都会被反复强调。

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 的处理流程是:

  1. responses 字典中取出 model 对应的 Pydantic 模型;
  2. 为它生成 JSON Schema;
  3. 把 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 中被多处复用,也让下游的代码生成工具能拿到完整的模型定义。

对于上面的例子,生成的 /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 中会出现完整的模型定义,MessageItem 都只在这里定义一次,别处一律用 $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 的元素是 stringinteger 的联合类型,并附带 inputctx 等字段。完整断言可查看 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

这里的关键点:

  1. responses[200] 里我们没有提供 model,而是直接写入 OpenAPI 原生结构 content.image/png(空对象即可,图片没有 JSON Schema 可言);
  2. 实际返回图片的代码路径中,必须直接返回 FileResponse("image.png", media_type="image/png"),才能让响应头真正带上 image/png
  3. 由于 response_model=Item 依然生效,FastAPI 会为 200 补上 application/json 分支的 Schema,最终 200 下同时存在 image/pngapplication/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_modelstatus_coderesponses

附加响应声明并非只能单独使用。你完全可以把 response_modelstatus_coderesponses 三个维度的信息组合起来:

  • 先用 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):

  • 404description 变成我们指定的 "The item was not found",并因 model: Message 自动带上 $ref: #/components/schemas/Messagecontent
  • 200description 变为 "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 把自定义的 descriptioncontentexample 等字段合并上去,从而既保留自动 Schema 又不丢用户信息。

最终,所有信息都会汇入 OpenAPI 并显示在 API 文档中。运行该应用后打开 /docs,你会看到 GET /items/{item_id} 的 Responses 区域同时列出 200404,各自携带不同的描述与示例:

FastAPI Swagger UI 中 GET /items/{item_id} 的 200 与 404 响应各自显示自定义描述与示例

复用预定义响应:字典解包技巧

当项目中存在大量 path operations 都需要携带同一组「预定义响应」(例如统一的 404302403 描述)时,逐条复制会非常啰嗦。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 快照可以看到最终结果:404302403 各自带有预定义的描述;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,覆盖 1xx5xx 等信息)取值,取不到时再回退到 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:其内可写字段包括 descriptionheaderscontent(在这里声明不同 media type 及其 JSON Schema)、links 等——responses 参数中每个状态码的 value 字典,理论上都能直接放进这些字段。

需要注意的是,附加响应声明中 model 键是 FastAPI 特有的便捷入口,而 descriptionheaderscontentlinks 等都是合法的 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 子类(JSONResponseFileResponse 等);其二,model 是 FastAPI 扩展键,其余字段遵循 OpenAPI 规范。这两条约束分别由 fastapi/routing.py 的字段预构建和 fastapi/openapi/utils.py 的文档合并逻辑保障,仓库内 tests/test_tutorial/test_additional_responses/ 下的四个测试文件则为每一份示例的运行时行为与最终 OpenAPI 输出提供了可重复的验证基准。建议你在自己的项目中用 /openapi.json 反复核对生成的契约,确保文档与真实返回保持一致。

登录后查看全文
热门项目推荐
相关项目推荐