encoding/json/v2 转正:默认拒绝重复 key 与非法 UTF-8,旧代码怎么过渡
encoding/json/v2 随 Go 1.27 转正(2026-08-19)。新包提供 Marshal、MarshalWrite、MarshalEncode、Unmarshal、UnmarshalRead、UnmarshalDecode,全部接受可变 Options;配套的 encoding/json/jsontext 提供基于 Token/Value 状态机的 Encoder/Decoder。
v2 默认更严格
两处默认行为变化最容易在线上暴露:
- JSON 字符串中的非法 UTF-8 直接拒绝,不再静默替换。
- 同一对象内的重名 key 直接拒绝,不再后者覆盖前者。
package main
import (
"fmt"
"encoding/json/v2"
)
func main() {
// 同一对象内重复 key:v2 默认拒绝
var m map[string]int
err := json.Unmarshal([]byte(`{"a":1,"a":2}`), &m)
fmt.Println(m, err)
// 字符串值里含非法 UTF-8 字节:v2 默认拒绝
var s struct{ S string }
err = json.Unmarshal([]byte("{\"S\":\"\xff\"}"), &s)
fmt.Println(s, err)
}
这两条不需要通过 Options 打开,是默认语义。对上游数据不可控的服务,重复 key 与坏编码从「悄悄放过」变成「显式报错」,需要确认调用方能否接受。
v1 由 v2 支撑,错误文本可能变
encoding/json(v1)现在由 v2 实现支撑:编解码行为保留,但错误信息文本可能不同。用错误字符串做分支判断或做断言比对的代码要重新核对。v1 API 继续支持,不要求迁移。
v1 包新增了若干 Options,可以在调用点让 v2 按 v1 语义运行。这是渐进迁移的主要手段:先定位差异点,再在具体调用点固定旧语义,而不是整体改写。性能上 Marshal 大致相当,Unmarshal 明显更快。
临时回退
构建时设置 GOEXPERIMENT=nojsonv2 可恢复旧的 v1 实现。该开关未来会移除,只适合作为短期兜底,不适合长期依赖。
GOEXPERIMENT 期间被移除与变更的项
- 移除
formattag 选项(#79071) - 移除
unknowntag(#77271) - 移除
DiscardUnknownMembersmarshal 选项(#77271) - 移除
SkipFunc哨兵错误(#74324) inline改名embed(#79985)stringtag 行为更新(#79065)MatchCaseInsensitiveNames行为更新(CL 792780)jsontext数值 Token 访问器同时返回error(#77666)
inline 到 embed 是纯改名,全局替换即可:
// 旧
type User struct {
Base
Name string `json:"name"`
Meta string `json:",inline"`
}
// 新
type User struct {
Base
Name string `json:"name"`
Meta string `json:",embed"`
}
string tag 的行为更新(#79065)要按字段回归:凡是标了 json:",string" 的字段,编码结果在边界情况下可能与之前不同。MatchCaseInsensitiveNames 的行为更新(CL 792780)则影响依赖大小写不敏感字段名匹配的代码路径,按名字匹配字段的逻辑需要单独验证。
jsontext 的数值访问器
jsontext 的数值 Token 访问器现在同时返回 error(#77666)。原来直接取值的调用点要改成两值接收,并处理解析失败:
// 示意:数值访问器现在返回 (值, error)
if tok.Kind() == jsontext.KindNumber {
n, err := tok.Int64()
if err != nil {
// 超出范围或不是合法整数表示
}
_ = n
}
迁移检查清单
- 先不传
Options跑全量测试,记录被拒的输入与依赖错误文本的断言。 - 针对具体调用点用 v1 新增的
Options固定旧语义,逐点收敛,不做全局一刀切。 - 全局搜
json:",inline",改为json:",embed"。 - 对
json:",string"字段做编码回归,比对输出差异。 - 检查
MatchCaseInsensitiveNames相关路径的匹配结果。 - 替换已移除的
formattag、unknowntag、DiscardUnknownMembersmarshal 选项、SkipFunc哨兵错误。 - 检查
jsontext里所有数值 Token 取值处,补上error分支。 - 确认线上是否存在重复 key 或非法 UTF-8 的历史数据;必要时在入口层清洗,而不是靠 v2 放行。
- 回退方案写明
GOEXPERIMENT=nojsonv2,并标注该开关会被移除的时限约束。