Swagger 接口文档精读手册

Swagger 接口文档精读手册


序:为什么值得花时间读这个

你手上有一份 Swagger 文档(/v2/api-docs),几十个接口、二十几个模型、几万字节。

新手会做的事:打开 Swagger UI,看到一排接口,点"Try it out",填参数,发送。

问题:Swagger UI 可能被拦了(302 到登录页)。而且——即使能打开,UI 渲染出来的表单也可能是错的,因为文档本身是坏的。

读完这本手册你能做到:

能力 具体表现
一眼看穿文档质量 知道哪些接口的文档能信、哪些必须自己摸
从 JSON 直接推出请求 不依赖 Swagger UI,直接写 Burp 包
判断参数放 URL 还是 body 这是新手最容易错的地方,第 8 章专门讲
让服务端告诉你答案 用报错信息反推后端注解(黄金技巧)
把文档当攻击面地图用 字段名、枚举值、状态机全在里面

一句话:Swagger 文档是攻击面地图,但它常常是画错的地图。你要学会的不是"照着走",而是"边走边校正"。


第 1 章 心智模型:文档的三大块

一份 Swagger 2.0 文档,本质就是一个 JSON。别看它有几万字节,结构只有三块:

{
  "swagger": "2.0",
  "host": "demo.example.com",     ← ① 元信息:告诉你怎么拼地址
  "basePath": "/app",
  "tags": [ ... ],

  "paths": { ... },               ← ② 接口清单:每个 URL 干什么
  "definitions": { ... }          ← ③ 数据模型仓库:字段结构都在这
}

真实地址公式(最重要的一条)

真实地址 = {scheme}://{host}{basePath}{paths 里的键}

套到示例:

https://demo.example.com  +  /app  +  /report/exportExcel
= https://demo.example.com/app/report/exportExcel

注意:basePath 不是自动的。有些文档 basePath 是 /,有些是空。永远先看 basePath 再拼地址。

definitions 是什么

字段结构的仓库。paths 里描述接口时,不重复写字段结构,而是写一个指针指过去:

"schema": { "$ref": "#/definitions/UserVo" }

→ "请求体的结构,去 definitions 里找 UserVo 那一格"

这就是第 6 章要讲的 $ref。


第 2 章 拆一个接口:逐字段精读

拿一个有教学价值的接口当标本:

"/report/exportExcel": {
  "get": {
    "tags": ["report-controller"],
    "summary": "管理端-报表列表-导出Excel",
    "description": "exportExcel",
    "operationId": "exportExcelUsingGET",
    "consumes": ["application/json"],
    "produces": ["*/*"],
    "parameters": [ { "in": "body", "name": "params", ... } ],
    "responses": { "200": { "description": "OK" } }
  }
}

逐个字段的含义与「该不该看」

字段 值 含义 该不该看
键名 /report/exportExcel URL 路径,拼地址用 ✅ 必须
get — HTTP 方法,这个接口只认 GET ✅ 必须
tags ["report-controller"] 分组标签 = 后端 @Api(tags=)。几十个接口全是同一个 tag ⇒ 所有接口挤在一个 Controller 类里 ⚪ 参考
summary 管理端-报表列表-导出Excel 中文简述。格式是「角色-模块-动作」 ⇒ 管理端 三个字直接告诉你权限归属 ✅ 最重要
description exportExcel 详细描述。这里只是把 Java 方法名重复了一遍 ⇒ 零信息量(作者偷懒) ❌ 忽略
operationId exportExcelUsingGET 唯一 ID = Java 方法名 + Using + 方法名 ⇒ 反推源码 public ... exportExcel(...) ⚪ 参考
consumes ["application/json"] 声明"请求体是 JSON" ⚠️ 这里可能是假的,见第 8 章
produces ["*/*"] 声明"响应类型任意"。不写 application/json 而写 */*,恰恰说明返回的是二进制流(Excel 文件) ✅ 有用
parameters [ {...} ] 参数列表 ✅ 必须
responses {"200":{"description":"OK"}} 响应定义。只有状态码没有 schema ⇒ 因为返回文件流,Swagger 描述不了 ⇒ 你从文档看不到导出结果长什么样 ⚪ 参考

作者的观察

这套文档里,summary 的信息量 > 其他所有字段加起来。

因为 summary 里的「用户端 / 管理端」是权限归属的唯一线索。而 description 和 operationId 是自动生成的噪声。

读文档的第一优先级:把所有 summary 抄下来,按「用户端 / 管理端 / 公共」分类。 这一步就能直接产出你的越权测试清单。


第 3 章 构造一条请求,只需要回答三个问题

这是本手册的核心框架。看完这一章,你就知道该在文档里找什么了。

问题 1:参数放哪?   → 看 parameters[].in
问题 2:参数叫什么? → 看 parameters[].name
问题 3:参数什么类型?→ 看 parameters[].schema 或 type

三条答案凑齐,一条 HTTP 请求就出来了:

POST /app/order/submit?query参数                 ← 问题 1(query 部分)
Host: demo.example.com
Content-Type: application/json

{"字段名": 值}                                    ← 问题 1(body 部分)+ 问题 2 + 问题 3

三个问题的难度完全不同

问题 难度 为什么
1. 放哪 ⭐⭐⭐ 最难 文档经常写错(第 8 章)
2. 叫什么 ⭐⭐ 中等 断链/压平的接口拿不到字段名(第 5、6 章)
3. 什么类型 ⭐ 简单 type/format 直接写,只是偶尔非法

新手 90% 的挫败感来自问题 1。 因为文档说 in: body,你发了 body,服务端说"参数为空"——然后你就卡住了。


第 4 章 schema:结构说明书

一句话定义

schema 回答的是「这块数据长什么样」——有哪些字段、每个字段什么类型。

关键理解:只有「一整个对象」才需要 schema

看两个真实参数的对照:

in: query 的参数 —— 没有 schema

{
  "name": "outOrderNo",
  "in": "query",
  "required": true,
  "type": "string"          ← 就一个标量值,type 一句话说完
}

in: body 的参数 —— 有 schema

{
  "in": "body",
  "name": "params",
  "schema": {               ← 因为 body 是"一整个对象",得单独描述它的内部
    "$ref": "#/definitions/UserVo"
  }
}

规律表:

参数位置 值的形态 用什么描述
query / path / header 一个标量("abc"、123) type 直接写
body 一整个 JSON 对象 {...} schema

schema 出现的 4 个位置

位置 描述的是
parameters[].schema 请求体的结构
responses["200"].schema 响应体的结构
definitions.<名字> 本身就是一份 schema(被 $ref 引用的仓库)
properties.<字段>.items.schema 数组元素的结构

一条完整的 schema 链(真实数据)

以 GET /app/user/queryPage 为例,请求和响应各有一条链:

请求链:

parameters[0].schema
   └─ $ref → #/definitions/UserVo      ⇒ 请求体字段 = UserVo 的 17 个字段

响应链:

responses["200"].schema
   └─ $ref → #/definitions/Result«Page«UserVo»»
        ├─ properties.code      : integer(int32)
        ├─ properties.message   : string
        ├─ properties.success   : boolean
        └─ properties.result    → $ref → #/definitions/Page«UserVo»
                ├─ current : integer(int32)
                ├─ size    : integer(int32)
                ├─ total   : integer(int64)
                ├─ pages   : integer(int64)
                └─ records : type=array
                        └─ items.$ref → #/definitions/UserVo   ← 绕回同一个模型

读法:Result 包一层 → Page 包一层 → 里面是 UserVo 的数组。

这个响应的真实长相:

{
  "code": 200,
  "message": "操作成功",
  "success": true,
  "result": {
    "current": 1,
    "size": 10,
    "total": 523,
    "records": [
      { "userId": 1, "name": "...", "cardNo": "...", ... }
    ]
  }
}

有趣的观察:请求和响应最终都指向 UserVo ⇒ 这个接口的「查询条件」和「返回记录」用的是同一个模型。这也解释了为什么它的参数能写出来(17 个字段),而 exportExcel 那个写不出来(断链)。

type / format / $ref 三者的分工

这是理解 schema 的关键:

写法 含义 对应 Java 类型
"type":"string" 字符串 String
"type":"integer","format":"int32" 32 位整数 Integer
"type":"integer","format":"int64" 64 位整数 Long
"type":"boolean" 布尔 Boolean
"type":"array","items":{...} 数组 List<T>
"$ref":"#/definitions/X" 自定义对象 你自己写的类 X

记忆法:

type / format = 标量(语言自带类型)
$ref = 对象(自定义类型)

Swagger 2.0 的合法 type 只有 7 个:

string · number · integer · boolean · array · object · file

没有 int。看到 "type":"int" 就是非法的(第 9 章会讲为什么会这样)。


第 5 章 $ref 与断链

$ref 是指针

{ "$ref": "#/definitions/UserVo" }

读法:# = 本文档根,/definitions/UserVo = 顺着这个路径找过去。

类比 HTML 锚点:

<a href="#sec3">跳到第三章</a>
...
<h2 id="sec3">第三章</h2>     ← 这个 id 必须存在

id="sec3" 不存在,链接就是死的。

断链(dangling reference)

指针指向了一个不存在的位置。

真实实例:

"paths": {
  "/report/exportExcel": {
    "get": {
      "parameters": [{
        "schema": { "$ref": "#/definitions/审核查询参数" }   ← 指针
      }]
    }
  }
},

"definitions": {
  "UserVo":  { ... 17 个字段 ... },      ← 有 ✔
  "OrderVo": { ... 22 个字段 ... },      ← 有 ✔
  // 审核查询参数                        ← 指针要的,这里没有 ✘
}

⇒ 断链。文档在 parameters 处告诉你"去 definitions 找 X",但 X 不存在。

为什么会产生断链(两种成因)

成因 A:手写注解把类型名写错了

审核查询参数 是中文名字。Java 类名不可能是中文 ⇒ 它不可能是一个真实的类。

只能来自手写注解:

@ApiImplicitParam(
    name = "params",
    dataType = "审核查询参数",   // ← 手写了个字符串
    paramType = "body"
)

Springfox 照抄这个字符串生成 $ref,但没有对应的 Java 类可以生成定义 → 断链。

成因 B:JDK 类型被当成了模型

"schema": { "$ref": "#/definitions/Long" }     ← userId 参数
"schema": { "$ref": "#/definitions/Model" }    ← 某些无类型接口
"schema": { "$ref": "#/definitions/String" }
"schema": { "$ref": "#/definitions/List" }

Long / String / List 是 Java 语言自带类型。Springfox 推断出参数类型是 Long,就按"模型"格式生成了指针 —— 但它永远不会为 JDK 类型生成 definitions ⇒ 必然断链。

Model 更明显:这是 Springfox 的兜底占位符,意思是"我知道有个参数,但我不认识它是什么类型"。

自己查断链(5 行代码)

import fs from 'fs';
const doc = JSON.parse(fs.readFileSync('api-docs.json', 'utf8'));
const refs = new Set();
JSON.stringify(doc).replace(/"#\/definitions\/([^"]+)"/g, (m, g) => { refs.add(g); return m; });
const defs = new Set(Object.keys(doc.definitions || {}));
console.log('断链:', [...refs].filter(r => !defs.has(r)));

典型输出:

断链: 审核查询参数, FeeDetailVo, Model, Long, List, String

这个检查应该在做任何 Swagger 分析之前先跑一遍 —— 它直接告诉你「哪些接口的文档可以信」。

断链的情报价值

断链不只是文档质量问题,它能反推后端代码:

断链名字 反推出的后端实现
审核查询参数 用了 @ApiImplicitParam 手写注解,dataType 写了中文 ⇒ 真实参数类型未知(可能是 Map、@RequestBody String、或散装参数)
FeeDetailVo 有个真实 VO 类,但没被 Springfox 扫描到(缺 @ApiModel 或不在扫描包内)⇒ 这个类的结构从没打算公开
Long / String / List 参数是散装的 JDK 类型,没有包装 DTO
Model Springfox 完全不认识这个参数类型(Object / Map / 泛型擦除)

实际价值:断链的接口,参数对自动化工具是黑盒 —— 也意味着自动化扫描器打不进去,手工摸反而可能有收获。


第 6 章 name 的陷阱:body 里要不要传 params?

直接回答:不要。

body 里直接平铺字段,不要在外面套一层 "params"。

为什么?因为 name 只在「键值对」参数里才有意义

HTTP 请求有两种传参形态:

形态 1:键值对(query / path / header / formData)

GET /x?outOrderNo=123&amount=1
       └─────┬─────┘
             └─ 这就是 name,服务端靠它 getParameter("outOrderNo")

name 是协议的一部分 —— 名字错了就取不到值。

形态 2:整个对象(body)

POST /x
Content-Type: application/json

{"userId":1,"status":1}
 └──────┬──────┘
        └─ body 本身就是这个对象,没有外层键名

服务端拿到 body 后:

objectMapper.readValue(body, UserVo.class)

直接把 body 内容映射成对象 —— 中间没有任何"键名"环节。

对照表

in name 是什么 请求里怎么写
query URL 上的键名 ?outOrderNo=123
path 路径段的名字 /user/{userId} → /user/1
header HTTP 头名 X-Token: abc
formData 表单字段名 a=1&b=2
body 只是标签,没用 直接写 {...},不套外层

规律:name 只在「键值对」形式里有意义。body 是一整个对象,不是键值对,所以 name 退化成说明性标签。

name: "params" 从哪来?

从 Java 方法参数名抄来的:

public Result<Page<UserVo>> queryPage(
        @RequestBody UserVo params) {     // ← 方法参数名叫 params

破绽:注意这一项:

{ "name": "params", "description": "params" }

description 本来应该是人写的说明。这里跟 name 一模一样 ⇒ 没人手写过,全是 Springfox 自动填的。

(对比 UserVo 里的字段:"description": "证件号码" —— 那是开发者真的写了 @ApiModelProperty(value="证件号码")。)

⇒ 看到 name == description,就知道这个字段是纯自动生成的,可以忽略。

错误 vs 正确

❌ 错误:外面套一层

POST /app/user/queryPage HTTP/1.1
Content-Type: application/json

{"params":{"userId":1}}

服务端把 body 反序列化成 UserVo,去找有没有 params 字段 → 没有 → 丢弃(或报 Unrecognized field)⇒ 等于什么都没传。

✅ 正确:直接平铺

POST /app/user/queryPage HTTP/1.1
Content-Type: application/json

{"userId":1,"status":1}

name 在 body 上有用吗?

在 Swagger 2.0 规范里,in: body 的参数永远是"整个请求体就是它",不存在需要外层键名的场景。

name 在 body 上只剩两个作用:

用途 说明
文档展示 Swagger UI 拿它当标签显示
工具内部变量名 Postman 导入时用它命名变量

对手工构造请求:完全忽略它。


第 7 章 parameters 为什么会有多个 in: body?

看一个接口的文档,你会发现一个奇怪的现象:

14 个参数,全部 in: body
 第 1–7 项    description 是英文(orderNo / planId / ...)      ← 无用组
 第 8–14 项   description 是中文(订单号 / 方案Id / ...)        ← 有用组

核心纠正:14 个 in: body ≠ 14 个 body

它们是「同一个 body 里的 7 个字段」,而且被写了两遍。

规范上应该长这样

"parameters": [{
  "in": "body",
  "name": "params",
  "schema": {
    "type": "object",
    "properties": {
      "orderNo":    { "type": "string" },
      "planId":     { "type": "integer" },
      "attachA":    { "type": "string" },
      "attachAName":{ "type": "string" },
      "attachB":    { "type": "string" },
      "attachBName":{ "type": "string" },
      "orgId":      { "type": "integer" }
    },
    "required": ["orderNo","planId","attachA","attachAName","attachB","attachBName","orgId"]
  }
}]

一个 body,内部用 properties 描述 7 个字段。

实际写法为什么变成 14 项

开发者用了 @ApiImplicitParams 手写注解,把每个方法参数都声明了一遍:

@ApiImplicitParams({
  @ApiImplicitParam(name="orderNo", dataType="string", paramType="body"),
  @ApiImplicitParam(name="planId",  dataType="int",    paramType="body"),
  ...
})

Springfox 把这 7 条照抄进 parameters 数组 ⇒ 违反规范(in: body 只能有一个)⇒ 再叠上 Springfox 的模型合并 Bug,变成 14 条。

读法

✅ 看带中文 description 那组(有 required: true 和真实说明)
❌ 忽略 name == description 那组

附带的三个坑

坑 表现
非法类型 "type":"int" 不在 Swagger 合法类型里(应为 integer)⇒ 工具可能报错或忽略
类型冲突 orgId 第一组是 int,第二组是 string ⇒ 必须两个都试
描述写反 attachB = "附件B名称"、attachBName = "附件B" ⇒ 与 attachA/attachAName 的规律相反 ⇒ 语义要实测

第 8 章 核心难题:参数放 URL 还是 body?

这是新手最容易卡住的地方。 因为文档说 in: body,你发了 body,服务端说"参数为空"。

8.1 第一步:看 in(文档给的线索)

in 的值 参数位置 长相
query URL ?a=1&b=2
path URL 路径 /user/{id} → /user/1
header HTTP 头 X-Token: abc
formData 表单 a=1&b=2(x-www-form-urlencoded)
body 请求体 {"a":1}

8.2 第二步:理解「为什么 in 会骗人」——Springfox 的默认行为

这是你困惑的根本原因。

后端 Java 写法 Springfox 标成 可信度
@RequestParam String orderNo in: query ✅ 可信
@RequestBody OrderDto dto in: body + $ref 指向 DTO ✅ 可信
@PathVariable Long id in: path ✅ 可信
String orderNo, Integer planId(什么都不写) in: body + {"type":"string"} 不可信!

最后一行就是坑:开发者写了散装参数但没加注解,Springfox 无法确定它是 query 还是 form,于是保守地默认标成 in: body —— 但它实际运行时会被 Spring MVC 绑到 query string 上。

8.3 第三步:破绽识别(不用实测就能先猜)

特征 判断
in: query + type: string ✅ URL,直接信
in: body + 单个 $ref 指向存在的 DTO ✅ body(JSON),直接信
in: body + schema 只有 {"type":"string"} / {"type":"int"} 可疑,极可能是 URL
in: body + 多个参数(像上面那样 14 个) 可疑(规范上 body 只能 1 个)
in: body + type: "int"(非法类型名) 可疑(手写注解,不是自动推断)
in: body + HTTP 方法是 GET 可疑(GET 规范上不该有 body)
consumes: application/json + 单个 $ref ✅ body

规律总结:

in: body 且 schema 是一个对象引用($ref) → 真 body
in: body 且 schema 是一个标量类型(string/int) → 假 body,实际是 URL

8.4 第四步(决定性):报错信息会「直接告诉你后端注解」⭐

这是本手册最有价值的一张表。

Spring 的框架层报错会把后端用了什么注解暴露出来:

你看到的报错 后端真实注解 参数应该在
Required String parameter 'orderNo' is not present @RequestParam URL
Required request body is missing @RequestBody body
Content type 'application/x-www-form-urlencoded' not supported @RequestBody body(JSON)
Content type 'application/json' not supported @ModelAttribute form
JSON parse error: Unrecognized field "orderNo" @RequestBody body 位置对了,字段名错
参数异常,必填参数为空 业务层自己写的校验 位置可能对了(已进业务代码)
Field error in object 'X' on field 'y': rejected value [z] @ModelAttribute 字段名 + 期望类型全泄露

Required String parameter 'xxx' is not present 是黄金信号 —— 出现它,说明参数必须在 URL 上,别再试 body 了。

Field error in object ... on field 'xxx' 是第二个黄金信号 —— 它一次泄露一个字段名,是摸断链接口字段名的最佳手段。

8.5 第五步:实战决策流程

拿到一个接口
│
├─ 文档写 in: query ────────────────────→ 拼 URL,直接发
│
├─ 文档写 in: body + $ref(存在)────────→ 发 JSON body
│
├─ 文档写 in: body + {"type":"string"} ──→ 可疑,进入"三发对比"
│
└─ 三发对比(决定性):
     包 A(JSON body 版)
     POST /app/order/submit
     Content-Type: application/json
     {"orderNo":"A001","planId":1,...}
     
     包 B(URL 版)
     POST /app/order/submit?orderNo=A001&planId=1&...
     
     包 C(form 版)
     POST /app/order/submit
     Content-Type: application/x-www-form-urlencoded
     orderNo=A001&planId=1&...
     
     → 哪个返回业务数据/业务错误,哪个就是对的

三个包唯一的差别就是"参数放哪",一次就能定位。

8.6 HTTP 方法也能给线索

方法 参数通常在哪 原因
GET URL 规范上 GET 不该有 body,且要能缓存/分享
POST URL 或 body 都可能 取决于后端注解
PUT / PATCH body(倾向) 语义是"提交一个资源"
DELETE URL 通常只带一个 id

⇒ GET 接口标 in: body,基本可以直接判定文档错了,优先试 URL。


第 9 章 文档的 10 种病(识别清单)

把前面的坑汇总成一张体检表。拿到任何 Swagger 文档,按这张表过一遍。

# 病症 长相 后果
1 未授权可读 生产环境 /v2/api-docs 直接 200 全量接口 + 字段 + 枚举暴露
2 $ref 断链 $ref 指向 definitions 里不存在的键 拿不到字段结构
3 schema 被压平 {"type":"string"} 描述整个 DTO 字段名全丢
4 GET 标 in: body GET 接口 + in: body 误导调用方
5 参数重复两遍 name 组 + description 组 不知道看哪组(看有中文描述的)
6 非法 type "type":"int" 工具报错/忽略
7 类型自相矛盾 同一字段两组 type 不同 必须两个都试
8 描述写反 attachB/attachBName 语义互换 字段语义要实测
9 JDK 类型当模型 $ref: #/definitions/Long 必然断链
10 GET 声明 consumes: application/json 语义错误 忽略即可

第 10 章 从文档到请求:完整工作流

① 读元信息
   swagger / host / basePath  → 记下地址公式

② 跑断链检查(5 行脚本)
   → 知道哪些接口文档能信

③ 抄所有 summary
   → 按「用户端 / 管理端 / 公共」分类
   → 这就是你的越权测试清单

④ 找目标接口,读 parameters
   ├─ in: query        → 参数名直接用
   ├─ in: body + $ref  → 去 definitions 拿字段
   └─ in: body + type  → 可疑,准备三发对比

⑤ 读 responses.schema
   → 知道返回结构,判断成功/失败的标志

⑥ 构造请求(三发对比法)
   A: JSON body
   B: URL query
   C: form-urlencoded

⑦ 读报错信息定位
   → 黄金信号表反推后端注解
   → 如果报 Field error,白送字段名

附录 A 一页纸速查表

地址拼接

真实地址 = {scheme}://{host}{basePath}{paths 键}

参数三问

问题 看哪
放哪 parameters[].in
叫什么 parameters[].name
什么类型 parameters[].type / schema

in → 位置

in 位置
query URL ?a=1
path URL 路径 /x/{id}
header HTTP 头
formData 表单
body 请求体(name 忽略)

类型对照

Swagger Java
string String
integer + int32 Integer
integer + int64 Long
boolean Boolean
array + items List<T>
$ref 自定义类

合法 type 只有 7 个:string number integer boolean array object file

in: body 真假判定

schema 长相 真假
$ref → 存在的 DTO ✅ 真 body
$ref → 不存在的(断链) ❌ 假,或字段未知
{"type":"string"} ❌ 假,实际在 URL

黄金信号(报错 → 注解)

报错 注解 位置
Required String parameter 'x' is not present @RequestParam URL
Required request body is missing @RequestBody body
Content type 'x-www-form-urlencoded' not supported @RequestBody body
Content type 'application/json' not supported @ModelAttribute form
JSON parse error: Unrecognized field @RequestBody body,字段名错
Field error in object 'X' on field 'y' @ModelAttribute 字段名泄露

文档质量分档

档 长相 能照抄吗
✅ QUERY-可用 in:query + type 能
✅ BODY-可用 in:body + $ref 存在 能
❌ BODY-断链 in:body + $ref 不存在 不能
❌ BODY-空schema in:body + {"type":"string"} 不能
⚠️ 无参数 parameters: [] 不代表真没参数

附录 B 两个自检脚本

B1 断链检查

// _dangling.mjs
import fs from 'fs';
const doc = JSON.parse(fs.readFileSync('api-docs.json', 'utf8'));
const refs = new Set();
JSON.stringify(doc).replace(/"#\/definitions\/([^"]+)"/g, (m, g) => { refs.add(g); return m; });
const defs = new Set(Object.keys(doc.definitions || {}));
console.log('断链:', [...refs].filter(r => !defs.has(r)));
console.log('未被引用:', [...defs].filter(r => !refs.has(r)));

B2 parameters 质量分档

// _paramtable.mjs
import fs from 'fs';
const doc = JSON.parse(fs.readFileSync('api-docs.json', 'utf8'));
const defs = doc.definitions || {};
for (const [path, ops] of Object.entries(doc.paths)) {
  for (const [method, op] of Object.entries(ops)) {
    const ps = op.parameters || [];
    const inb = ps.filter(x => x.in === 'body');
    const inq = ps.filter(x => x.in === 'query');
    let v = '无参数';
    if (inb.length) {
      const ref = inb[0].schema && inb[0].schema.$ref;
      if (ref) {
        const n = ref.replace('#/definitions/', '');
        v = defs[n] ? 'BODY-可用' : 'BODY-断链';
      } else if (inb[0].schema && inb[0].schema.properties) v = 'BODY-内联';
      else v = 'BODY-空schema';
    } else if (inq.length) v = 'QUERY-可用';
    console.log(method.toUpperCase().padEnd(7) + path.padEnd(45) + v);
  }
}

附录 C 核心心法(如果只记三句话)

1. in 只是线索,报错信息才是判据。
文档坏的时候,让服务端自己告诉你答案。

2. $ref 是对象,type/format 是标量。
看到 $ref 就去 definitions 找;找不到就是断链。

3. in: body 的 name 是标签,不是键名。
body 直接平铺,别套外层。


本手册为通用教学资料,所有域名、路径、类名、字段名、业务名称均为示意占位符。

posted @ 2026-09-23 23:00  0xMouise  阅读(18)  评论(0)    收藏  举报