Go JSON 键名变了
Go 1.27 的旧 JSON API 可能调用命名字符串 map 键的 MarshalText,改变输出键名。1.27.2 修复了直接 string 类键,指针与接口键需要另行验收。
升级 Go 1.27 后,encoding/json.Marshal 输出的 map 键多了前缀或经过其他转换,而同一份数据在 1.26 中使用原来的字符串。若键类型是命名的 string,并实现了 MarshalText,应检查 JSON 兼容实现是否改变了方法调用顺序。
Go 1.27 默认由新的 JSON v2 实现支持旧 encoding/json API,旧 API 仍应保留自己的行为约定。命名字符串键误调用 MarshalText 的修正已包含在 1.27.2;直接使用 encoding/json/v2 则有不同的方法调用契约,不能混在一起判断。
map 键类型比输出内容更有用
下面的示例体现键名的两种形成方式:底层字符串 red,以及方法生成的 C_red。
type Color string
func (c Color) MarshalText() ([]byte, error) {
return []byte("C_" + string(c)), nil
}
var colors = map[Color]int{Color("red"): 1}
这只是类型片段。调用旧 encoding/json.Marshal 时,原有约定是 string 类型的键直接使用字符串值;只有相应的其他键类型才按 encoding.TextMarshaler 规则处理。不能仅凭 Color 实现了方法,就推断旧 API 必须使用它。
Go 1.27.1 的相关兼容路径会调用这里的方法,输出键因而发生变化。1.27.2 对旧语义下的 string 类键加上了方法跳过判断,使这条直接命名字符串路径恢复原约定。
先记录工具链、构建实验设置与实际 import 路径,再看 map 的声明类型。map[Color]、指针键和接口包装键不是同一种情况;后两类还有独立的兼容回归边界,不能因命名字符串修复就认定全部已恢复。
按业务需要明确选择键的契约
需要保持旧字符串键时,采用包含直接字符串键修复的 Go 1.27.2 或后续合适版本,重新构建实际应用,再对照输出。也可以在这条序列化边界明确转换为 map[string],保留原来的底层字符串值,而不依赖自定义方法优先级。
如果业务本来就需要转换后的名称,应明确计算发送给外部的字符串键,并处理转换后名称冲突。不要让两个业务键静默覆盖同一个 JSON 名称,也不要用正则替换已序列化的 JSON 来补救。
不宜直接删除类型的 MarshalText。它可能被其他值、接口或编码过程使用;修一条 map 输出路径,不应连带改变那些消费者。
临时旧实现开关要留在构建范围
Go 1.27 提供构建时的 GOEXPERIMENT=nojsonv2,可以暂时恢复原 v1 实现,用于兼容性处理或比较。但采用新 v2、jsontext API 的项目还需核对能否关闭这一实验;不能把开关复制为所有项目的固定配置。
这个选择需要重新构建,不是给运行中的进程设置环境变量就生效。它也会改变其他 JSON 行为,且该退出方式计划在后续版本移除,因此不能代替明确的序列化契约和维护版本修正。
检查线上实际发送的键
核对真实构建产物输出的 JSON 名称、下游查找、接口 schema、签名或持久化格式。涉及自定义转换时覆盖碰撞和 Unicode 情况,同时检查该类型其他 marshal、unmarshal 用途没有改变。
只确认本地某个方法被调用或未被调用,还不足以证明接口兼容。已经保存的旧 JSON 也不会因为升级而重新编码;需要迁移历史数据时,应单独保留业务规则与原始记录。
修改前保存构建配置和代表性载荷。出现回归时恢复经过审定的兼容构建与约定,不批量改写历史键名来掩盖当前输出变化。


