首页
/ FastAPI 定制 OpenAPI Schema:覆盖 app.openapi() 与 get_openapi() 工具函数的完整实践

FastAPI 定制 OpenAPI Schema:覆盖 app.openapi() 与 get_openapi() 工具函数的完整实践

2026-09-06 16:31:17作者:宗隆裙

在需要为 API 文档加入自定义扩展(如 ReDoc 的 x-logo 标志、企业级元数据)时,FastAPI 允许在保持默认行为的基础上直接干预 OpenAPI Schema 的生成过程。本文基于官方文档 "Extending OpenAPI",结合 fastapi/applications.pyfastapi/openapi/utils.py 源码,讲清楚默认 Schema 的生成链路、get_openapi() 的完整参数、以及如何用“生成—修改—缓存—替换方法”四步完成对 .openapi() 的安全覆盖。

一、默认流程:OpenAPI Schema 从哪里来

一个 FastAPI 应用实例带有一个 .openapi() 方法,它负责返回应用的 OpenAPI Schema。在应用对象创建阶段,setup() 会注册一个 /openapi.json(即 openapi_url 配置值)路径操作,其处理函数直接返回 self.openapi() 结果的 JSON 响应。这一点可以在 setup() 实现 中确认:

def setup(self) -> None:
    if self.openapi_url:

        async def openapi(req: Request) -> JSONResponse:
            root_path = req.scope.get("root_path", "").rstrip("/")
            schema = self.openapi()
            # 若启用了 root_path_in_servers,会把 root_path 注入 servers
            ...
            return JSONResponse(schema)

        self.add_route(self.openapi_url, openapi, include_in_schema=False)

默认情况下,.openapi() 的逻辑是:检查属性 .openapi_schema 是否已有内容,有则直接返回;没有则调用工具函数 fastapi.openapi.utils.get_openapi 生成。当前源码实现(fastapi/applications.py#L1070-L1103)还额外引入了路由版本检查——当路由树发生变化(routes_version 改变)时,缓存会失效并重新生成:

def openapi(self) -> dict[str, Any]:
    routes_version = self.router._get_routes_version()
    if not self.openapi_schema or self._openapi_routes_version != routes_version:
        self.openapi_schema = get_openapi(
            title=self.title,
            version=self.version,
            openapi_version=self.openapi_version,
            summary=self.summary,
            description=self.description,
            ...
            routes=self.routes,
            webhooks=self.webhooks.routes,
            ...
        )
        self._openapi_routes_version = routes_version
    return self.openapi_schema

这说明框架本身已经内置了“缓存 + 失效”机制,官方文档中教我们手动实现缓存,正是复现并延伸这一默认行为。

get_openapi() 的参数说明

文档明确列出的核心参数如下:

参数 含义
title OpenAPI 标题,显示在文档页面
version 你的 API 版本号,例如 2.5.0
openapi_version 使用的 OpenAPI 规范版本,默认取最新值 3.1.0
summary API 的简短摘要
description API 描述,支持 Markdown,会显示在文档页面
routes 应用的路由,取自 app.routes。FastAPI 用它收集已注册的 path operations,包括各 router 纳入的路由

从源码签名看(get_openapi 定义),get_openapi() 还支持一批文档未逐一展开的可选参数,在需要更深定制时可以一并利用:

def get_openapi(
    *,
    title: str,
    version: str,
    openapi_version: str = "3.1.0",
    summary: str | None = None,
    description: str | None = None,
    routes: Sequence[BaseRoute | routing.RouteContext],
    webhooks: Sequence[BaseRoute | routing.RouteContext] | None = None,
    tags: list[dict[str, Any]] | None = None,
    servers: list[dict[str, str | Any]] | None = None,
    terms_of_service: str | None = None,
    contact: dict[str, str | Any] | None = None,
    license_info: dict[str, str | Any] | None = None,
    separate_input_output_schemas: bool = True,
    external_docs: dict[str, Any] | None = None,
) -> dict[str, Any]:

两个值得注意的细节:

  • summary 的版本要求summary 字段由 OpenAPI 3.1.0 规范引入,FastAPI 自 0.99.0 起支持(源码中 if summary: info["summary"] = summaryutils.py#L602-L604)。
  • app.routes 是低层路由树:它可能包含 FastAPI 为 included routers 使用的内部路由候选,而不只是最终的 APIRoute 对象。从源码结构看,get_openapi() 通过 routing.iter_route_contexts(routes) 递归遍历该路由树,收集真正生效的 path operations,因此直接把 app.routes 传给它没有问题。

二、覆盖默认行为:生成—修改—缓存—替换

核心思路:用同一个 get_openapi() 工具函数生成基础 Schema,然后按需修改其中的任意部分。下面以 ReDoc 的 OpenAPI 扩展为例,给 info 对象添加自定义 x-logo 字段。

第 1 步:按正常方式编写 FastAPI 应用

应用代码本身不需要任何特殊处理(对应 docs_src/extending_openapi/tutorial001_py310.py):

from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi

app = FastAPI()


@app.get("/items/")
async def read_items():
    return [{"name": "Foo"}]

第 2 步:用工具函数生成 OpenAPI Schema

custom_openapi() 函数内调用 get_openapi(),传入你需要的 titleversionsummarydescriptionroutes=app.routes

def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
    openapi_schema = get_openapi(
        title="Custom title",
        version="2.5.0",
        summary="This is a very custom OpenAPI schema",
        description="Here's a longer description of the custom **OpenAPI** schema",
        routes=app.routes,
    )

第 3 步:修改 Schema

此时 Schema 就是一个普通的 Python dict,可以直接增改任意字段。本例添加 ReDoc 的 x-logo 扩展:

    openapi_schema["info"]["x-logo"] = {
        "url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"
    }

第 4 步:把 Schema 写入 .openapi_schema 作为缓存

.openapi_schema 属性存储生成结果,这样应用不必在每次有人打开 API 文档时都重新生成 Schema——它只生成一次,后续请求直接返回缓存:

    app.openapi_schema = openapi_schema
    return app.openapi_schema

第 5 步:替换 .openapi() 方法

最后一行把应用的方法整体替换为你的自定义函数:

app.openapi = custom_openapi

完整示例即为 tutorial001_py310.py 全文。启动后访问 http://127.0.0.1:8000/redoc,可以看到 ReDoc 页面左上角使用了你配置的自定义 Logo(示例中使用的是 FastAPI 的 Logo);访问 /docs 则能看到定制的 titlesummary 与 Markdown 描述的 description

三、源码级验证:缓存、路由版本与测试用例

官方测试如何验证定制效果

仓库自带针对该示例的测试 tests/test_tutorial/test_extending_openapi/test_tutorial001.py,它做了两件事:

  1. 请求 /openapi.json,用快照断言确认返回的 info 中确实包含定制的 titlesummarydescription 以及 x-logo 扩展;
  2. 再次请求 /openapi.json,断言两次返回完全一致——以此验证自定义缓存路径(直接命中 app.openapi_schema 早返回)工作正常。
def test_openapi_schema():
    response = client.get("/openapi.json")
    assert response.json() == snapshot({
        "openapi": "3.1.0",
        "info": {
            "title": "Custom title",
            "summary": "This is a very custom OpenAPI schema",
            "description": "Here's a longer description of the custom **OpenAPI** schema",
            "version": "2.5.0",
            "x-logo": {"url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"},
        },
        "paths": {"/items/": {"get": {...}}},
    })

默认流程中缓存的细节

对比默认实现可以看到,手动缓存(if app.openapi_schema: return app.openapi_schema)与框架内置缓存是同一设计意图,但框架版本更严谨:它在路由树版本变化时会丢弃旧 Schema 重新生成(applications.py#L1084-L1095)。如果你采用手动覆盖方案,且运行期间通过 app.include_router() 等方式动态注册新路由,需要自行评估是否也要处理失效逻辑。

/openapi.json 响应的附加行为

setup() 源码 还可以看到:当请求携带 root_path(如部署在反向代理子路径下)且启用了 root_path_in_servers 时,框架会把 root_path 注入 servers 字段后再返回。这一行为发生在 .openapi() 之外,因此你手动定制 Schema 时不受影响,两者互不干扰。

四、小结:适用场景与注意事项

  • 适用场景:向 info 注入文档工具专属扩展(x-logox- 前缀自定义字段)、重写 servers / tags / externalDocs 等默认不暴露给 FastAPI() 构造参数的字段,或对整份 Schema 做程序化改写。
  • 实现要点:始终复用 get_openapi() 而非从零拼装 dict,保证路径、参数、组件等结构与官方生成完全一致;修改操作只作用于其返回的 dict 副本语义上,最后写回 app.openapi_schema 并执行 app.openapi = custom_openapi
  • 限制summary 仅在 OpenAPI 3.1.0+ 下进入输出;直接替换方法后,框架内置的“路由版本变化自动失效”缓存策略不再作用于你的自定义实现,长期动态变更路由的场景需谨慎。

掌握以上链路后,你可以对 /openapi.json 的输出内容做到任意程度、可测试、可复现的精确控制。

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