FastAPI 定制 OpenAPI Schema:覆盖 app.openapi() 与 get_openapi() 工具函数的完整实践
在需要为 API 文档加入自定义扩展(如 ReDoc 的 x-logo 标志、企业级元数据)时,FastAPI 允许在保持默认行为的基础上直接干预 OpenAPI Schema 的生成过程。本文基于官方文档 "Extending OpenAPI",结合 fastapi/applications.py 与 fastapi/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"] = summary,utils.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(),传入你需要的 title、version、summary、description 与 routes=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 则能看到定制的 title、summary 与 Markdown 描述的 description。
三、源码级验证:缓存、路由版本与测试用例
官方测试如何验证定制效果
仓库自带针对该示例的测试 tests/test_tutorial/test_extending_openapi/test_tutorial001.py,它做了两件事:
- 请求
/openapi.json,用快照断言确认返回的info中确实包含定制的title、summary、description以及x-logo扩展; - 再次请求
/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-logo、x-前缀自定义字段)、重写servers/tags/externalDocs等默认不暴露给FastAPI()构造参数的字段,或对整份 Schema 做程序化改写。 - 实现要点:始终复用
get_openapi()而非从零拼装 dict,保证路径、参数、组件等结构与官方生成完全一致;修改操作只作用于其返回的 dict 副本语义上,最后写回app.openapi_schema并执行app.openapi = custom_openapi。 - 限制:
summary仅在 OpenAPI 3.1.0+ 下进入输出;直接替换方法后,框架内置的“路由版本变化自动失效”缓存策略不再作用于你的自定义实现,长期动态变更路由的场景需谨慎。
掌握以上链路后,你可以对 /openapi.json 的输出内容做到任意程度、可测试、可复现的精确控制。
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 StartedRust0624
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