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: 链接已创建

顶层必填字段只有两个,openapiinfo;此外文档还必须包含 pathscomponentswebhooks 三者中的至少一个。这是 OAS 3.1.1 规范的明确要求。

开头这几行里就埋着一个坑:openapiinfo.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 序列;
  • 标签支持层级,新增 summaryparentkind
  • 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/,而不是整个目录。

从哪里开始

如果接口已经有了、描述还没有,大致按这个顺序来:

  1. 手写一个端点的描述。这就足够看懂结构了。
  2. 选一个工具链稳妥支持的版本,默认 3.1。
  3. 文件一屏装不下时再用 $ref 拆开,同时立刻定下它从哪个地址提供。
  4. 加一个双向比对规范与路由的测试。在这一步之前,任何关于「文档始终是最新的」的说法都只是承诺,不是属性。

想看看真实接口上是什么样子,可以翻 Lix.li 的 API 文档,里面涵盖链接、分组、A/B 测试和转化。围绕这套接口的服务见短链接 API 页面;相邻话题还有什么是 webhook301、302、307、308 跳转与请求方法的拆解,以及用 PHP SDK 创建短链接的实例。