OpenAPI 是一种用普通 YAML 或 JSON 文件描述 HTTP 接口的格式。文件里写明接口暴露了哪些路径、每个路径接受什么方法、请求里传什么字段、响应又返回什么。这个文件人能读,程序也能读,所以文档、客户端库和测试用的模拟服务都可以从同一份源文件生成。
它的正式名称是 OpenAPI Specification,简称 OAS,由 Linux 基金会下的 OpenAPI Initiative 以开放治理的方式维护。
下面讲文档怎么组织、OpenAPI 和 Swagger 到底差在哪、3.1 与 3.2 改了什么,以及为什么这个格式最核心的承诺——文档永远不会过期——单靠它自己并不成立。
一份 OpenAPI 文档长什么样
能跑起来的文件比多数人想的要短:
openapi: 3.1.0
info:
title: 链接
version: "1.0"
paths:
/links:
post:
summary: 创建短链接
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [url]
properties:
url:
type: string
format: uri
responses:
'201':
description: 链接已创建
顶层必填字段只有两个,openapi 和 info;此外文档还必须包含 paths、components、webhooks 三者中的至少一个。这是 OAS 3.1.1 规范的明确要求。
开头这几行里就埋着一个坑:openapi 和 info.version 完全是两回事。前者是格式本身的版本,工具靠它判断该怎么解析这份文档;后者才是你这套接口的版本。规范写得很清楚,但直觉正好相反,于是就出现了声明 openapi: 1.0 的文件。
规模大一些的规范通常用 $ref 拆成多个文件:根文档指向 ./paths/links.yaml,后者再指向 ./schemas/link.yaml。这样很方便,也带来一个问题,留到最后再说。
OpenAPI 和 Swagger 不是一回事
这是本话题里最常见的混淆。
| 是什么 | 谁在维护 | |
|---|---|---|
| OpenAPI | 标准本身,也就是这种格式 | OpenAPI Initiative,Linux 基金会 |
| Swagger | 读取这种格式的一套工具 | SmartBear |
历史能解释这团乱麻。最早 Swagger 既指规范也指工具。后来规范被捐给 OpenAPI Initiative,正式更名为 OpenAPI Specification,而 Swagger 这个名字留给了工具——Swagger UI、Swagger Editor 等等。
落到实处,「我们用了 Swagger」几乎总是意味着「我们有一份 OpenAPI 文件,用 Swagger UI 把它渲染出来」。两者之间没有绑定关系:同一份文件用 Redoc、Scalar 或者客户端生成器打开,效果一样。
版本:3.0、3.1 和 3.2
各分支之间的差别不是表面功夫。
3.0 目前部署最广。它的短板在数据模型上:写法像 JSON Schema,却又不完全兼容,工具之间的互操作性因此反复出问题。
3.1 补上了这个缺口。Schema 对象成为 JSON Schema Draft 2020-12 的超集,于是你已经用 JSON Schema 维护的那些模型可以直接复用,不必重写。当前修订版 3.1.1 发布于 2024 年 10 月 24 日。
3.2 在 2025 年 9 月发布。按照 OpenAPI Initiative 的公告,比较值得注意的有:
query方法,用于参数塞不进 URL 的幂等读取;additionalOperations,用于非标准方法;- 流式媒体类型:Server-Sent Events、JSON Lines、JSON 序列;
- 标签支持层级,新增
summary、parent和kind; - OAuth 2.0 设备流,面向输入不便的设备。
选版本要看工具链实际支持到哪一步,而不是挑数字最大的。生态对 3.1 的支持已经很扎实,3.2 较新,部分工具还在跟进。我们自己的规范声明的是 3.1.0。
这份文件实际能换来什么
一个文件同时解决好几件事:
- 交互式文档:Swagger UI 或 Redoc 直接按文件生成页面,还带「试一试」的请求表单;
- 客户端库:生成器按你的语言产出 SDK,不用手写一层层包装;
- 模拟服务:后端还没写完,就能照着描述把 mock 服务跑起来,前端不必干等;
- 契约测试:测试拿真实响应和 schema 对照,响应一旦和描述对不上就能立刻发现。
支持 OpenAPI 的主要论据就出自这份清单:文档不再是一份单独的、总有人忘记更新的文字。
论据本身没错。问题是它不会自动兑现。
规范和代码之间没有任何联系
这是「什么是 OpenAPI」这类文章几乎不提、真上了生产却会咬人的部分。
规范文件就是一个文件。路由不知道它的存在,控制器也不知道。你可以删掉一个接口、加一个接口,或者把必填字段改成选填——不会有任何一个测试失败。文档不会自己同步,它准确的时长,恰好等于有人记得去改它的时长。
偏差会朝两个方向发生,各有各的害处:
- 写了但用不了。 开发者照着文档发请求,收到 404。对其余文档的信任当场崩塌——这里既然是错的,别处呢?
- 能用但没写。 对于正在找它的人来说,这个能力根本不存在。没人发现,也就没人为它付费。
第二种更危险,因为没人会来抱怨。也没人可抱怨:用户压根不知道这个接口存在。
我们就栽在这上面。Lix.li 的接口里有三个转化相关的端点在正常工作——一个事件列表,加上单条和批量的回传接收——却没有一个出现在规范里。代码是好的,测试也通过,而与此同时 API 落地页上还展示着两个代码里根本不存在的路径。这件事是在一次全面核对中发现的,不是靠谁报的故障。
怎样自动发现偏差
靠自觉治不了,得靠测试。思路很直白:把规范里的路径列出来,把路由里的规则列出来,然后双向比对这两个集合。
openapi.yaml 里的路径 → 有对应路由吗? → 没有,说明文档在骗人
路由里的规则 → 规范写了吗? → 没有,说明功能是隐形的
「写了的确实能用」这一半,用真实请求验证最省事:不带凭证调用接口,确认返回的是 401 而不是 404。这个区别很关键。401 表示路由找到了、需要密钥;404 表示压根没有这个路径。整个过程不会产生任何写入,因为鉴权在控制器之前就执行了。
反向那一半,读路由配置即可。这一步还会逼你把例外显式列出来——那些不该出现在公开契约里的内部端点。这份例外清单本身就有价值:它迫使你认真决定一次,究竟什么才算你的公开接口。
转化那件事之后我们写了这个测试。它立刻就证明了自己:把转化相关的路径从规范里删掉,测试就会失败,并且会把两个缺失的路径逐一点名。
两个发现得太晚的坑
$ref 和相对路径。 规范一旦拆成多个文件,内部引用就会相对于文件被提供时的 URL 去解析。一份放在 /openapi 的根文档,会去 /paths/links.yaml 找 ./paths/links.yaml,然后什么也找不到。在浏览器里看一切正常,因为 Swagger UI 是从文件真实所在的位置加载的;可是把同一份文档喂给客户端生成器,就会出错。要么只从规范自己的目录提供它,要么打包成不含外部 $ref 的单文件。
Swagger UI 在爬虫眼里是空的。 Swagger UI 页面不过是几行标记加一段脚本,内容是在浏览器里画出来的。源码 HTML 里一个接口路径都没有。对读者来说页面正常,对索引来说它是空白。再加上 robots.txt 里一个常见错误:本意是屏蔽 JSON 接口而写的 Disallow: /api/,会把同一前缀下给人看的文档一并屏蔽掉。应该屏蔽带版本号的精确路径,比如 /api/1.0/,而不是整个目录。
从哪里开始
如果接口已经有了、描述还没有,大致按这个顺序来:
- 手写一个端点的描述。这就足够看懂结构了。
- 选一个工具链稳妥支持的版本,默认 3.1。
- 文件一屏装不下时再用
$ref拆开,同时立刻定下它从哪个地址提供。 - 加一个双向比对规范与路由的测试。在这一步之前,任何关于「文档始终是最新的」的说法都只是承诺,不是属性。
想看看真实接口上是什么样子,可以翻 Lix.li 的 API 文档,里面涵盖链接、分组、A/B 测试和转化。围绕这套接口的服务见短链接 API 页面;相邻话题还有什么是 webhook、301、302、307、308 跳转与请求方法的拆解,以及用 PHP SDK 创建短链接的实例。