FastAPI 扩展 OpenAPI 指南:自定义 `/openapi.json` 生成过程与完整实战
FastAPI 会自动为应用生成符合 OpenAPI 规范的 API 文档,而当你需要为文档注入自定义 Logo、修改标题描述、添加供应商扩展(Vendor Extensions)时,就必须介入默认的 OpenAPI 生成流程。本文基于仓库中的官方教程(葡萄牙语文档 docs/pt/docs/how-to/extending-openapi.md,英文原文见 docs/en/docs/how-to/extending-openapi.md)展开,结合 fastapi/openapi/utils.py 与 fastapi/applications.py 的源码实现,讲透 OpenAPI 的默认生成机制、get_openapi() 全部核心参数,并给出一个可复制、可运行的“自定义 Logo”完整示例,让你能随意改写 API 文档的任意部分。
默认的 OpenAPI 生成流程(The Normal Process)
在动手覆盖之前,必须先理解 FastAPI 默认是如何生成 OpenAPI 架构(schema)的。整个流程分为三步:
FastAPI应用实例拥有一个.openapi()方法,它负责返回 OpenAPI 架构(一个 Python 字典)。- 在创建应用对象时,会自动注册一条指向
/openapi.json的路径操作路由(路径取决于openapi_url参数,默认就是/openapi.json)。这条路由本身不做任何额外处理,只是把.openapi()方法的返回值以 JSON 响应返回。 - 默认情况下,
.openapi()方法会先检查.openapi_schema属性是否有内容:如果有,直接返回;如果没有,就调用工具函数fastapi.openapi.utils.get_openapi生成,并把结果缓存到.openapi_schema中。
换句话说,用户每次打开 Swagger UI(/docs)或 ReDoc(/redoc),页面都会请求 /openapi.json,最终由 .openapi() 方法给出 JSON 数据。
get_openapi() 的核心参数
get_openapi() 是生成 OpenAPI 架构的“发动机”,它的关键参数如下:
| 参数 | 含义 | 默认值 / 说明 |
|---|---|---|
title |
OpenAPI 的标题,显示在文档界面中 | 必填 |
version |
你的 API 版本,例如 2.5.0 |
必填 |
openapi_version |
使用的 OpenAPI 规范版本 | 默认是最新版 3.1.0 |
summary |
API 的简短摘要 | 可选;仅 OpenAPI 3.1.0 及以上支持(FastAPI 0.99.0 及以上) |
description |
API 的详细描述,支持 Markdown,会显示在文档中 | 可选 |
routes |
应用的路由,取自 app.routes,FastAPI 用它来收集所有已注册的路径操作,包括被 include_router 引入的子路由 |
必填 |
技术细节(Tip):
app.routes是一个底层路由树,其中可能包含 FastAPI 为被引入的 Router 内部使用的“候选路由”,而不仅仅是最终的APIRoute对象。不过你仍然可以把app.routes直接传给get_openapi()——FastAPI 会遍历这棵路由树,提取出真正生效的路径操作。该逻辑在 fastapi/openapi/utils.py 的get_fields_from_routes()与get_openapi()主函数中通过routing.iter_route_contexts()实现。
注意(Note):
summary参数从 OpenAPI 3.1.0 规范开始可用,对应 FastAPI 0.99.0 及以上版本才支持。
覆盖默认流程:自定义 OpenAPI 架构(Overriding the Defaults)
掌握了上述流程后,思路就非常清晰了:复用 get_openapi() 这个工具函数生成架构,再对返回的字典做任意修改,最后把应用默认的 .openapi() 方法替换成自己的函数。
下面以“为 ReDoc 添加自定义 Logo(x-logo 供应商扩展)”为例,演示完整过程。完整源码见 docs_src/extending_openapi/tutorial001_py310.py。
第一步:先写一个正常的 FastAPI 应用
照常编写应用,定义路由即可:
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
app = FastAPI()
@app.get("/items/")
async def read_items():
return [{"name": "Foo"}]
第二步:用同一个工具函数生成 OpenAPI 架构
在自定义函数 custom_openapi() 内部调用 get_openapi(),并传入自定义的 title、version、summary、description 以及 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,
)
# ... 后续修改与缓存步骤
注意这里复用的正是默认流程使用的 fastapi.openapi.utils.get_openapi,所以生成的架构与默认版本完全一致,只是应用了你自定义的元信息。
第三步:修改 OpenAPI 架构
现在可以对返回的字典做任意修改了。OpenAPI 架构的顶层包含 info、paths、components 等键,其中 info 是一个“对象”。我们往 info 里添加 ReDoc 支持的 x-logo 供应商扩展:
openapi_schema["info"]["x-logo"] = {
"url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"
}
x-logo 是 ReDoc 的官方供应商扩展(Vendor Extension),OpenAPI 规范允许以 x- 开头的自定义字段存在于规范允许的位置,ReDoc 会读取 info 下的 x-logo 并在页面右上角渲染该图片。
第四步:把生成的架构缓存起来
每次用户打开 API 文档都重新生成一次完整架构是浪费的。FastAPI 默认就使用 app.openapi_schema 属性作为缓存:首次生成后存入,后续请求直接复用同一个缓存对象。我们同样利用这个机制:
app.openapi_schema = openapi_schema
return app.openapi_schema
在 custom_openapi() 的开头(if app.openapi_schema: return app.openapi_schema)加上缓存检查,这样架构只生成一次,之后的请求全部命中缓存。
第五步:覆盖应用的 .openapi() 方法
把应用默认的 .openapi() 方法替换为我们的自定义函数:
app.openapi = custom_openapi
这一步是关键:因为 /openapi.json 路由在响应时调用的是 app.openapi(),替换之后,Swagger UI、ReDoc 以及任何直接请求 /openapi.json 的客户端拿到的都是我们定制后的架构。
完整的 custom_openapi() 函数如下(便于直接复制运行):
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,
)
openapi_schema["info"]["x-logo"] = {
"url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"
}
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
第六步:验证效果
启动应用(uvicorn 运行入口文件后),访问 http://127.0.0.1:8000/redoc,即可看到 ReDoc 页面右上角显示的是自定义 Logo(本例中为 FastAPI 的 Logo):
源码级原理:默认 .openapi() 到底做了什么
为了让覆盖过程更有把握,我们直接看 fastapi/applications.py 中 FastAPI.openapi() 的实现。它的核心逻辑是:
- 先通过
self.router._get_routes_version()获取当前路由版本号; - 如果
.openapi_schema为空,或路由版本发生了变化(self._openapi_routes_version != routes_version),就调用get_openapi()重新生成; - 生成时把
title、version、openapi_version、summary、description、terms_of_service、contact、license_info、routes、webhooks、tags、servers、separate_input_output_schemas、external_docs等应用属性全部透传给get_openapi(); - 记录路由版本号,返回缓存的
self.openapi_schema。
值得注意的是第 2 点:FastAPI 内置的缓存并不是“一劳永逸”的,当检测到路由有变化时会自动重新生成架构。而文档示例中的自定义 custom_openapi() 只检查 app.openapi_schema 是否有内容,属于“永久缓存”策略——如果你的应用会在运行期动态添加路由,可以考虑在自定义实现里同样引入版本号检查。
再看 /openapi.json 路由的注册,位于 fastapi/applications.py 的 setup() 方法:如果设置了 openapi_url,FastAPI 会注册一个内部 openapi 协程路由,它从请求中读取 root_path,调用 self.openapi() 获取架构,在需要时把 root_path 注入 servers,最后用 JSONResponse 返回。这也解释了为什么替换 app.openapi 就能全局生效——该路由响应时调用的正是这个属性。
get_openapi() 的生成过程
在 fastapi/openapi/utils.py 中,get_openapi() 的生成流程大致为:
- 组装顶层
info对象(title、version、summary、description、termsOfService、contact、license); - 初始化
openapi(版本号)、servers、paths、webhooks、components等顶层结构; - 收集所有路由涉及的请求体字段、响应字段、路径/查询/请求头/Cookie 参数,统一扁平化为模型(flat models)并生成
definitions(即components/schemas); - 逐个遍历路由树中的
APIRoute,通过get_openapi_path()生成每个路径下的 operation(方法、标签、参数、请求体、响应、回调、校验错误422响应等); - 处理 Webhooks 路由、
tags、externalDocs; - 最终通过
OpenAPI(**output)模型 +jsonable_encoder序列化为字典输出。
理解这一点后,你会发现“扩展 OpenAPI”其实就是三步套路:生成 → 修改字典 → 缓存并替换方法。无论你想改 info、paths、components 中的任何内容,还是往任意层级塞 x- 供应商扩展,都适用这一套路。
测试验证:仓库如何保证自定义逻辑正确
仓库为这个教程示例提供了完整的自动化测试,见 tests/test_tutorial/test_extending_openapi/test_tutorial001.py。该测试使用 TestClient 断言:
GET /items/返回[{"name": "Foo"}],证明业务路由不受影响;GET /openapi.json返回的架构中,info.title为"Custom title"、info.summary为自定义摘要、info.version为"2.5.0",且info下确实包含了x-logo字段;- 连续请求两次
/openapi.json得到完全一致的结果,验证了自定义缓存逻辑(架构只生成一次)的有效性。
如果你在自己项目中实现了类似的扩展,可以参考这个测试模式:既校验业务接口不受影响,又精确校验 OpenAPI 输出结构,同时验证缓存行为。
更进一步:还能扩展什么
掌握了“生成 → 修改 → 缓存 → 替换”的完整套路后,你还可以轻松实现这些常见定制:
- 修改 API 文档的品牌信息:替换
info中的title、description、contact、license,甚至加入x-logo; - 给整个文档补充
tags、externalDocs:get_openapi()支持这些参数,但默认的app.openapi只透传应用创建时传入的属性,自定义函数中可以额外补充; - 按需过滤或改写
paths:比如剔除内部接口、给路径加统一前缀、改写operationId等; - 注入自定义供应商扩展:如
x-code-samples(代码示例)、x-logo等 ReDoc/Swagger UI 支持的扩展字段。
值得注意的是,FastAPI 也提供了更轻量的声明式方案:在路径操作层面用 openapi_extra 直接合并额外字段(见 fastapi/openapi/utils.py 的 deep_dict_update(operation, route.openapi_extra))。当你的定制只影响单个接口时,openapi_extra 往往比全局覆盖更简洁;而需要全局统一改动时,本文的 .openapi() 覆盖方案则是最佳选择。
总结
- 默认情况下,
/openapi.json路由调用app.openapi(),其内部使用fastapi.openapi.utils.get_openapi生成架构,并用app.openapi_schema做缓存。 - 自定义扩展的标准做法:在
custom_openapi()中先检查缓存,再调用get_openapi(title=..., version=..., summary=..., description=..., routes=app.routes)生成字典,直接修改字典内容,写回app.openapi_schema缓存,最后用app.openapi = custom_openapi完成替换。 - 仓库中的示例源码 docs_src/extending_openapi/tutorial001_py310.py 与测试 tests/test_tutorial/test_extending_openapi/test_tutorial001.py 可直接作为你项目中的参考模板。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
