
Go 默认忽略 JSON 中的重复键,但可通过自定义 UnmarshalJSON 方法结合 json.Decoder 扫描原始 token 流,在首次赋值后检测并拒绝重复键,从而确保数据完整性与语义严谨性。
go 默认忽略 json 中的重复键,但可通过自定义 `unmarshaljson` 方法结合 `json.decoder` 扫描原始 token 流,在首次赋值后检测并拒绝重复键,从而确保数据完整性与语义严谨性。
在 Go 的标准 encoding/json 包中,当 JSON 对象包含多个同名字段(如 "name":"a", "name":"b")时,反序列化会静默覆盖——仅保留最后一个值,且不报错。这种“宽容”行为虽简化了兼容性处理,但在配置校验、协议验证或安全敏感场景下可能引发隐蔽逻辑错误。因此,需主动检测并拒绝非法重复键。
✅ 推荐方案:基于 json.Decoder.Token() 的逐词法扫描
最可靠的方式是绕过结构体自动绑定,改用低层 json.Decoder 遍历 JSON token 流,手动维护字段赋值状态。以下是一个健壮、可复用的示例:
package main
import (
"bytes"
"encoding/json"
"fmt"
)
type Gauge struct {
Name string `json:"name"`
}
func (g *Gauge) UnmarshalJSON(data []byte) error {
dec := json.NewDecoder(bytes.NewReader(data))
if tok, err := dec.Token(); err != nil || tok != json.Delim('{') {
return fmt.Errorf("expected object start: %w", err)
}
var seenName bool
var key, value string
for dec.More() {
tok, err := dec.Token()
if err != nil {
return err
}
switch t := tok.(type) {
case string:
if key == "" {
key = t // 记录字段名
} else {
value = t // 记录字符串值
if key == "name" {
if seenName {
return fmt.Errorf(`duplicate key "name" in JSON object`)
}
g.Name = value
seenName = true
}
key = ""
value = ""
}
case json.Delim:
if t == '}' {
return nil
}
// 忽略数字、布尔、null 等非字符串值(可根据需要扩展)
}
}
return nil
}⚠️ 注意事项:
Json Schema Toolkit下载使用 JSON Schema 验证 JSON 数据,从示例 JSON 生成 schema,并将其转换为 TypeScript 接口、Python 数据类或 Markdown 文档。
- 此方法仅检测 string 类型的 "name" 字段重复;若需支持 number/boolean,需在 case json.Number 或 case bool 分支中补充逻辑。
- dec.Token() 会跳过空白与注释(JSON 标准不支持注释,但部分工具生成含注释 JSON),生产环境建议先做预清洗。
- 若结构体嵌套复杂,建议封装为通用 StrictObjectDecoder 工具函数,避免重复样板代码。
? 替代思路:单次赋值类型包装(适用于简单字段)
对单一字段强约束场景,可定义带状态检查的自定义类型:
type SingleAssignString string
func (s *SingleAssignString) UnmarshalJSON(data []byte) error {
str := strings.Trim(string(data), `"`)
if *s != "" {
return fmt.Errorf("field already assigned: %q", *s)
}
*s = SingleAssignString(str)
return nil
}
type Gauge struct {
Name SingleAssignString `json:"name"`
}该方式简洁,但无法定位重复键在 JSON 中的位置,错误信息泛化,且不适用于嵌套结构或需组合校验的场景。
✅ 总结
- 默认行为不可靠:Go json.Unmarshal 不校验重复键,不应依赖其“自动去重”作为业务逻辑依据。
- 首选 decoder token 扫描:提供精确错误位置、灵活控制与完整语义校验能力。
- 生产建议:将严格模式封装为中间件或配置选项(如 StrictJSONUnmarshal(v interface{}, data []byte, strict bool)),兼顾兼容性与安全性。
通过主动拦截与显式校验,你不仅能捕获潜在的数据污染风险,还能提升 API 契约的可靠性与调试效率。


















