首页
/ FastAPI 声明 OpenAPI 额外响应:responses 参数、model 模型与多媒体类型实践

FastAPI 声明 OpenAPI 额外响应:responses 参数、model 模型与多媒体类型实践

2026-09-03 16:03:06作者:齐添朝

本文讲解如何借助路径操作装饰器的 responses 参数,在 FastAPI 中为 OpenAPI 文档声明额外的状态码、媒体类型与描述信息,并深入到 fastapi/routing.pyfastapi/openapi/utils.py 的源码层,还原 model 键如何被解析成 JSON Schema、与 response_model 自动生成的内容如何合并,以及预定义响应如何通过字典解包在各接口间复用。

需要先说明:这是一个偏进阶的主题。如果你刚开始使用 FastAPI,通常暂时用不到这些能力。

为什么需要额外响应

默认情况下,FastAPI 只根据路径操作的 status_coderesponse_model 生成一条主响应。但真实 API 往往还需要向调用方声明:

  • 其他可能的状态码(如 404403302)及其响应结构;
  • 同一响应可返回的多种媒体类型(如 JSON 或 PNG 图片);
  • 自定义的描述、示例(example)、头(headers)、链接(links)等 OpenAPI Response Object 字段。

这些额外响应(additional responses)会被完整写入 OpenAPI 文档,因此也会自动出现在 Swagger UI / ReDoc 等 API 文档界面中。

一个关键约束:对于额外响应,你必须直接返回一个 Response 对象(如 JSONResponseFileResponse),并自行带上状态码和内容。FastAPI 在运行期不会对额外响应做校验或序列化,responses 参数仅服务于文档生成。

从源码看,这个约束的起点在 fastapi/routing.pyroute.responses = responses or {},装饰器传入的字典被原样挂在路由对象上,等待 OpenAPI 生成阶段处理。

model 的额外响应

你可以在 路径操作装饰器 上传入参数 responses。它接收一个 dict键是每个响应的状态码(如 200404),值是包含该响应信息的另一个 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

两点注意事项:

  1. 你必须直接返回 JSONResponse(连同状态码与内容)。
  2. model不是 OpenAPI 规范的一部分。FastAPI 只是从那里取出 Pydantic 模型、生成 JSON Schema,再把它放到正确的位置。

Schema 被放入的"正确位置"是:

  • content 键,其值是一个 JSON 对象(dict),其中包含:
    • 一个媒体类型键,例如 application/json,其值又是一个 JSON 对象,其中包含:
      • schema 键,其值即为该模型的 JSON Schema——这里就是正确位置。
        • FastAPI 在此处不会直接内联 Schema,而是添加一个指向 OpenAPI 其他位置全局 JSON Schema 的引用($ref)。这样其他应用和客户端可以直接使用这些 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 的状态码必须允许携带响应体,否则直接报错(即 204304 这类无体状态码不能声明 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 做快照断言,锁定 $refcomponents 结构。

为主响应声明多种媒体类型

同一个 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

两点注意事项:

  1. 图片必须用 FileResponse 直接返回
  2. 媒体类型的默认规则:除非你在 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_modelstatus_coderesponses

你还可以把来自多个位置的响应信息合并到一起,包括 response_modelstatus_coderesponses 参数。

做法是:声明一个 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 文档中:

Swagger UI 中合并后的响应:200 显示自定义描述与示例值,404 显示自定义描述

合并机制在源码中对应 deep_dict_updatefastapi/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 中预定义 404302403 三个响应,在装饰器里解包后与 200 的图片媒体类型声明合并,最终该路径操作的文档将同时展示这四种响应。对应的断言见 tests/test_tutorial/test_additional_responses/test_tutorial004.py

responses 中可以写入哪些 OpenAPI 字段

要查看响应中究竟可以包含什么,可以查阅 OpenAPI 3.1 规范中的两个对象(此处不附外部链接,规范名称如下即可检索):

  • Responses Object:响应对象集合,包含其中的 Response Object
  • Response Object:其中的任何字段都可以直接放进 responses 参数的每一条响应里,包括 descriptionheaderscontent(在其中声明不同的媒体类型与 JSON Schema)、以及 links

结合源码行为,使用时需注意:

  • 每个状态码对应的值必须是 dictfastapi/routing.py 的断言);
  • model 是 FastAPI 专用扩展键,不属于 OpenAPI,生成文档时会被移除,且要求对应状态码允许携带响应体;
  • 键会被统一转为字符串并大写化,DEFAULT 会归一化为 OpenAPI 的 default 兜底键(fastapi/openapi/utils.py);
  • 你写入的 descriptionheaderscontentlinks 等标准字段会与自动生成的主响应做深合并,同名键以你提供的为准。

小结

  • responses 参数让你在不改变运行行为的前提下,为 OpenAPI 文档补充任意状态码、媒体类型、描述、示例与链接;
  • model 键把 Pydantic 模型接进文档生成链路:路由注册期创建序列化字段,OpenAPI 生成期写入 content -> 媒体类型 -> schema,并以 $ref 指向全局 components/schemas
  • 运行期你必须自己返回 JSONResponseFileResponseResponse 对象;
  • 借助 deep_dict_update 的深合并与 **dict 解包,预定义响应和 response_modelstatus_code 的信息可以无缝组合,形成完整、可直接驱动客户端代码生成的 API 文档。
登录后查看全文
热门项目推荐
相关项目推荐