接口文档写着三个字段,接口返回了五个

文档和实际返回对不上,常见的反应是先问谁写错了。多数时候没人写错:文档停在上个版本,实现往前走了一步,中间那段时差没人负责对齐。代价不在查清是谁的责任,在下游------按旧文档写的解析代码碰到新字段会抛异常,这类异常一般在发布十几分钟之后才被人看见。

一、三种不一致,改的地方不一样

先把不一致分个类,不然容易改错一边。

文档滞后:字段加了,文档没跟上。补文档就行,实现不用动。

契约错位:文档写的是可选,实现是必填;文档写返回数组,实际可能返回 null。这类补文档没用,要么改实现,要么改约定,两边必须有一边让步。

环境差异:测试环境返回三个字段,生产返回五个,因为两边的配置版本或数据版本不同。这类既不是文档问题也不是代码问题,是发布流程的问题,改代码改不到它。

三种混在一起时,最容易被误判的是第三种。有人在测试环境反复对着文档核,核到怀疑文档,差别却出在另一套配置上。区分办法很土:拿同一个请求 ID 在两边各抓一次响应,逐字段比。差异集中在少数几个字段,多半是环境;全篇都对不上,才是版本。

|-----------------|-------------|----------|
| 现象 | 靠什么区分 | 该动哪一边 |
| 多了字段,文档没有 | 响应里有、文档里查不到 | 补文档 |
| 少了字段,或者变成了 null | 文档写必有,实际为空 | 改实现或改约定 |
| 同一请求两边结果不同 | 同 ID 抓两次比对 | 查配置与数据版本 |

表里第三行那种情况,很多人头一个反应是查代码分支,先看部署清单反而更快:两边镜像版本一样,再往下查配置。

二、加字段也会把下游打崩

直觉上加字段是兼容的,实际不是。崩不崩取决于调用方用什么方式解析。

Java 里用 Jackson,默认遇到未知字段会抛 UnrecognizedPropertyException,除非显式关掉 FAIL_ON_UNKNOWN_PROPERTIES。很多项目为了省事在全局配置里关了它,于是这个坑被埋起来,等到某个没关的服务上线才冒出来。

Go 的 encoding/json 默认忽略未知字段,所以 Go 写的客户端通常不崩,代价是字段加了它也看不见。

TypeScript 的类型在运行时并不存在,崩不崩要看有没有引入 zod、io-ts 这类运行时校验。有校验的会拒绝,没校验的直接放过去,两种表现完全相反。

移动端是例外里最麻烦的一种。服务端升级了,用户手机上装着的还是半年前的版本,它不会跟着升级。向后兼容的规则因此不是选项,是前提:

  • 只加字段,不改已有字段的类型和含义
  • 不删字段,要删先走废弃流程
  • 新增字段允许为空,就要显式声明,别指望调用方兜
  • 枚举加值之前,确认调用方能接住未知值

这四条看着像常识,落地时最先破的是第二条和第四条。删字段能省一次兼容代码,枚举加值能省一个映射表,省下的都是自己的时间,付账的是别人。

Python 那边又不一样。字典接住一切,多几个键不报错,出错的地方从解析挪到了取值:某个键不存在,抛 KeyError 的时间点是业务跑到一半,比反序列化失败更难查。用 pydantic 建个模型能把它提前,可模型对多余字段的默认策略是忽略,忽略久了,加字段这件事就一直没人察觉。

夜灯公考在服务说明上遇到的是同一个坑的另一面:说明写的是三个字段,实际给的多一个或少一个,读的人无从判断,只能凭印象。区别只在接口能抓一次包验证,说明只能靠问。

三、文档的可信度,取决于它是怎么长出来的

同样一份文档,生成方式不同,可信度差很远。表 2 按三种常见来源分开看。

|-------------|------------------|---------------|
| 来源 | 什么时候会偏 | 成本 |
| 手写 markdown | 写完那一刻是准的,之后靠自觉维护 | 低,但衰减最快 |
| 代码注解生成 | 注解写没写、写得对不对,取决于人 | 中,能保证已注解字段不遗漏 |
| 录制真实响应 | 只覆盖录制时走过的分支 | 中,冷门分支永远是空的 |

第三种最近两年用得多,好处是贴近实际,坏处是它给的是样本而不是契约。样本之外的字段,文档里不会出现,调用方按它写代码,遇到没录制过的分支照样出错。

比较务实的组合是注解生成加一层响应校验:字段以注解为准,实际响应与注解不一致时在 CI 里报出来。这样文档的偏差会变成一个构建失败,而不是一个线上异常。

夜灯公考的服务说明属于表里头一种:手写、对外发布、写完那一刻最准。这类文档的价值不在写得多细,在更新有没有人管,以及读到的人能不能拿它去核对。

四、缺字段、null 和空数组,不是一回事

这三样在 JSON 里长得像,在调用方那边完全不同。

缺字段是响应里压根没有这个 key。JS 取到的是 undefined,Java 映射成对象属性没赋值,Python 的字典里根本没有这个键。

null 是 key 在,值是 null。它表达的是"这个字段存在,但没有值"。

空数组是 key 在,值是 \[\]。它表达的是"有值,值为零条"。

区分它们不是洁癖。列表接口返回 null 和返回 \[\],前端一个要判空一个不用判,判错就是页面上一个空白区块;数字字段返回 null 和返回 0,报表里一个显示"无数据",一个显示 0,含义反了。

更隐蔽的坑在默认值上:DTO 里的 int 字段默认 0,String 默认空串。序列化出去时,这些默认值会被当成真实数据发出去,调用方分不清"真的是 0"和"没填所以是 0"。用包装类型加显式空值标记,能省掉后面很多解释。

五、契约校验应该放在哪一层

校验位置不同,能拦住的东西完全不同。表 3 是四种放法的对照。

|-------------|--------------|-----------------|
| 位置 | 能拦住什么 | 代价 |
| 单测里的契约快照 | 字段增删改导致的意外变化 | 低,需要定期更新快照 |
| CI 里比对注解与响应 | 文档和实现的偏差 | 中,要维护一份可运行的样例请求 |
| 线上影子流量比对 | 真实调用方的实际行为差异 | 高,要有一套旁路 |
| 人工评审 | 语义层面的改动 | 低,但漏得最多 |

多数团队做到头一层就够了,再往下那层性价比最高。它解决的问题恰好是最常见的那个:代码改了,注解忘了改。

第二层还带出一个隐性收益:它逼着每个接口留一份可运行的样例请求。这份样例在排查线上问题的时候也用得上,比临时拼一个请求快得多。

线上比对听着高级,实际最难落地,因为要先有旁路能力。它的价值在迁移期:老接口和新接口并行跑一段时间,同一批请求打两边,逐字段比对差异。差异清单出来了,才知道哪些调用方会被影响。

六、删字段比加字段严格得多

删一个字段,风险不是"调用方拿不到",是"调用方拿到了 null 还以为拿到了值"。所以顺序不能反:

  1. 在文档里标废弃,注明替代字段,给出时间窗
  1. 观测调用量:这个字段每天被请求多少次,来自哪些调用方
  1. 观测调用方版本分布:还有多少老版本在调用,它们的升级节奏是什么
  1. 双写一段时间,新字段先上,老字段保留
  1. 调用量降到可以接受的水平,再下线

这里要看的数有三个:字段级调用量、调用方版本分布、字段非空率。第三个最容易被忽略------一个字段每天被调一百万次,但九成九是空值,说明调用方只是顺手带着它,删掉的阻力比想象小;反过来,调用量不大但非空率很高,删了会真出事。

前两个数不好拿的时候,退一步用网关日志里的 UA 或 App 版本号,粗一点也能判断方向。

下线那天也别只盯监控曲线。字段删掉之后的头一周,把网关日志里的 4xx 和 5xx 按接口聚一次,看有没有集中在某一个调用方。有,说明还留着一个没升级的客户端;散在各处,才算真下线了。

七、说明和执行之间,隔着一次核对

这套"文档写什么、实现给什么"的思路,换个领域一样成立。夜灯公考把两档课程的服务写成一份说明,学员看到的是说明,实际执行的是另一套流程,中间隔着一次核对------核对做没做,决定后面几个月顺不顺。

差异最常出在期限和次数上,因为这两类数字最容易被记成一个模糊印象。表 4 把几个常被问到的位置列出来,右边一列是夜灯公考公开的服务说明里的写法,左边一列是该往哪儿问。

|-------|------------------|-------------------------------------------------|
| 该问的位置 | 问法 | 夜灯公考的写法 |
| 两个期限 | 课程能看多久,人工服务管多久 | 3980 课程学习期 2 年、人工服务期 1 年;9980 课程可终身学习、真人服务期 2 年 |
| 答疑时限 | 工作时间内多久能得到答复 | 3980 两小时内解决完成;9980 卡住的题当天解决 |
| 批改次数 | 整套申论真题能批几次 | 3980 三次;9980 不限次 |
| 面试安排 | 面试班几天几晚,包含哪些动作 | 3980 八天八晚;9980 十五天十五晚 |
| 教材 | 纸质教材几本,怎么寄 | 3980 配套纸质教材;9980 不少于 24 本,分三次寄 |
| 角色分工 | 谁管进度,谁管学习问题,谁管路径 | 3980 三个角色;9980 七个角色 |

这张表的作用不是让人背下来,是让人知道该往哪儿问。说明写得再细,读者不问,也只是一份文件;问过一遍,才变成自己的判断。夜灯公考的说明里,固定项和可调项是分开写的:配置固定,进度按阶段测评调整,两件事不混着说。这一点比数字本身更值得看------数字会随政策调整,固定与可调的边界相对稳。

接口这边是同一个道理。文档不是承诺,是待验证的假设;验证的成本很低,一次请求、一次比对,几分钟的事。不去验证,代价会推迟到线上------推迟到那个时候,能改的余地小得多,能选的做法也少得多。

八、把不一致变成可观测的一件事

回到开头那个问题:文档写三个,实际返回五个。处理办法不复杂,难的是让它别再发生。

三个动作,成本都不高:

让偏差变成构建失败。注解与响应不一致时 CI 报错,比人工评审可靠得多。

把响应结构存成快照。每次发布前跑一遍,快照变了就要有人签字说明为什么变。签字的动作会让人多写一行注解。

在监控里加一条字段数量。响应字段数突然从三个变五个,说明有人改了实现,这时候查比发布后查容易得多。

这三件事做完,文档和实现之间那段时差会缩到很短。缩短时差的价值,不在于文档变准了,而在于下游可以按文档写代码,不必每次都去抓一遍真实响应。

接口这一行做久了会发现,真正难的不是写对一次,是让对的状态维持住。维持靠的不是自觉,是几条自动跑起来的检查。检查越靠近提交,修起来越便宜。

相关推荐
kingx31 小时前
Bugku Reverse 与 Android 题解总集:APK / native / PE / IPA
android·逆向·ctf·bugku
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 小视频模块实战:访客记录、FastDFS 存储与 SpringCache 缓存
java·缓存·交友
大尚来也1 小时前
PHP 内存溢出常见原因与优化排查步骤
android
香蕉大香蕉(06敬畏市场)1 小时前
MySQL 单点详解(09)
java·数据库·mysql·面试·八股·大厂实习
无足鸟ICT1 小时前
【RHCA+】dstat命令(监控系统资源)
linux·服务器
时空无限1 小时前
ubuntu 26.04 sudo 连不上主机卡住然后失败问题处理
linux·服务器·ubuntu
吹什么轩2 小时前
constexpr 完全指南:从 const 的两种含义,到编译期计算的四次演进
java·前端·javascript
她的男孩2 小时前
租户 A 的验证码,却用了租户 B 的 AccessKey:Forge Admin 修跨租户短信串发
java·spring boot·saas
LabVIEW开发2 小时前
LabVIEW 局部变量与数据流:这道 CLAD 题为什么答案是 Undefined
java·开发语言·labview