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 直接平铺,别套外层。
本手册为通用教学资料,所有域名、路径、类名、字段名、业务名称均为示意占位符。

浙公网安备 33010602011771号