升级 Go 1.27 后 JSON 接口的行为变化:v1 默默接受的输入,v2 默认报错
参考:Go 1.27 Release Notes
Go 1.27 引入了 encoding/json/v2 和 encoding/json/jsontext 两个包,同时把现有的 encoding/json 换成了 v2 实现作为后端。多数代码升级后表现不变,但会接受非法输入的那部分线上行为,可能在这类升级中暴露出来。
v1 容错、v2 报错的两类输入
v2 的默认值比 v1 更严格、更强调互操作性,其中两条最容易在接口联调里撞上:
- JSON 字符串里的非法 UTF-8。v1 会把非法字节静默替换成
U+FFFD,然后照常返回字符串;v2 直接拒绝。 - 同一个 JSON 对象内的重复键。v1 是后者覆盖前者,写得进去也读得出来;v2 视为错误。
这两类数据在真实系统里通常来自拼接字符串、老旧服务端、日志回放或第三方回调。v1 时期它们不会报错,所以很难在测试里被发现,往往要等到流量上来才出现。
encoding/json 已经跑在 v2 上
encoding/json 现在由 v2 实现支撑。序列化和反序列化的行为是保留的,但错误信息的具体文案可能会变。如果代码或告警系统依赖 err.Error() 的字符串做匹配、或者依赖错误文本里的位置描述,升级时要一并检查。
v1 API 会继续支持,不强制迁移到 v2 新接口。Marshal 性能与之前大致持平,Unmarshal 明显更快,但具体数字以自己压测为准。
定位方法
1. 看报错原文形态。 v2 的报错一般会带出错位置和具体原因,先在日志里把原始错误完整打出来,不要只留下 json: cannot unmarshal ... 这类被截断的前缀,再对照输入报文找出是哪个字符串或哪个重复键。
2. 用 encoding/json/jsontext 做流式校验。 jsontext 提供的是低层语法处理,Encoder / Decoder 以 Token 和 Value 序列操作 JSON,内部维护状态机保证产出或消费的序列是合法 JSON 文本。适合在网关或中间件里对可疑报文单独跑一遍语法校验,把问题数据先筛出来,而不是等它进到业务结构体才发现。
3. 按需开启 v1 语义的 Options。 encoding/json 新增了一批 Options,可以配置 v2 以 v1 的语义运行,不必为此整体迁移到新 API。适合改造面太大、需要分批推进的场景:先让接口恢复原有行为,再逐个消费方收紧。
4. GOEXPERIMENT=nojsonv2 兜底。 构建时设置这个环境变量会恢复到原来的 v1 实现,用于紧急回滚。这是临时手段,官方说明该 opt-out 预计会在未来版本移除。遇到兼容性问题也应提 issue。
v2 移除和改名的选项
在 GOEXPERIMENT 阶段,encoding/json/v2 有几处值得注意的变化:
- 移除
formattag 选项(#79071) - 移除
unknowntag 选项(#77271) - 移除
DiscardUnknownMembersmarshal 选项(#77271) - 移除
SkipFunc哨兵错误(#74324) inlinetag 选项改名为embed(#79985)stringtag 选项的行为有更新(#79065)MatchCaseInsensitiveNames选项行为有更新(CL 792780)- 在
encoding/json/jsontext中,数值型Token的访问器改为同时返回 error(#77666)
前四项是直接消失,代码里如果还写着这些 tag 或选项,编译期或运行期都会出问题;inline → embed 是纯改名,需要同步改结构体 tag。string 和大小写不敏感匹配属于行为调整,同一份配置在新版本下可能得到不同结果,升级后建议针对这两处补回归用例。
背景和更多细节见提案 issue;如需关闭新实现,请提 issue 说明。