JSON 重复键解码时为什么只剩最后一个值?
来源:17golang原创
时间:2026-11-13 09:45:30 439浏览 收藏
我第一次遇到这个现象,是在排查一个“配置明明传了两次,程序却只认最后一次”的接口。原始 JSON 是 {"mode":"safe","mode":"fast"},解码到 Go 的 map[string]string 后只剩 fast。这不是 map 随机丢数据,而是 v1 encoding/json 按 JSON 成员出现顺序处理,重复键的后值覆盖先值。
如果重复键来自外部请求,不要把“最后一个值”当成业务规则。先保留原文并检测重复名;只有明确接受兼容语义时,才让默认解码继续。
- v1
encoding/json允许重复键,简单字段通常由后值覆盖先值。 - 结构体和嵌套对象可能出现“覆盖”与“合并”两种结果,不能只看 map 示例。
- 安全边界上的 JSON 应在进入业务结构前检测重复键,并记录拒绝原因。
最小样例为什么只打印最后一个值
先不要从业务代码猜原因,用同一段输入分别解到 map 和结构体。下面的注释只解释关键断点,运行结果是文章中的示意输出,不是线上截图。
package main
import (
"encoding/json"
"fmt"
)
type Config struct {
Mode string `json:"mode"`
}
func main() {
raw := []byte(`{"mode":"safe","mode":"fast"}`)
var values map[string]string
// Unmarshal 按成员出现顺序写入 map,后一个 mode 会覆盖前一个。
if err := json.Unmarshal(raw, &values); err != nil {
panic(err) // 示例直接终止;服务代码应返回可定位的输入错误。
}
var cfg Config
// 结构体字段也按同一顺序匹配,最后一次赋值保留下来。
if err := json.Unmarshal(raw, &cfg); err != nil {
panic(err) // 真实接口应把错误和请求追踪号一起记录。
}
fmt.Println(values["mode"], cfg.Mode) // 示意输出:fast fast
}
关键点是“按顺序处理”,不是 Go map 的遍历顺序。JSON 对象里的两个 mode 已经在解码阶段落到同一个目标字段,后来的 fast 把先前的 safe 覆盖了。

map 覆盖和嵌套对象合并不是一回事
官方文档把重复键描述为“后值替换或合并先值”,具体表现取决于目标值类型。简单字符串、数字、布尔值最容易观察到覆盖;嵌套对象解到已有的 map 或结构体时,字段级处理可能让结果看起来像合并。
| 目标类型 | 观察到的现象 | 排查重点 |
|---|---|---|
map[string]string | 后一个同名字符串覆盖前一个 | 检查原文顺序和上游拼装逻辑 |
| 结构体字段 | 同一字段后赋值保留 | 确认 json tag 和大小写匹配 |
| 嵌套对象 | 不同子字段可能继续存在 | 不要把“合并”误判为安全去重 |
因此,看到“只剩最后一个值”时,先把目标类型、JSON tag、嵌套层级和重复键的出现顺序一起记下来。只打印 map,无法证明所有目标类型都只会简单覆盖。
进入业务前怎样把重复键变成明确错误
如果 JSON 来自浏览器、合作方或消息队列,重复键可能是上游拼接错误,也可能让不同解析器得出不同业务结果。更稳妥的做法是先保留原文,在语法校验通过后扫描每个对象的成员名;发现同一对象内重复名就拒绝,不把“最后值”交给权限、金额或路由逻辑。
工程上可以把“重复键检测”放在 HTTP 入站层或消息反序列化层,并把对象路径、键名、请求追踪号写入审计日志。日志不要记录完整敏感 payload,只记录足以定位的摘要。

兼容输入与不可信输入应该分开处理
内部旧接口如果已经约定“后值生效”,可以继续使用 v1 默认行为,但应在协议文档里写清楚,并增加重复键测试。外部不可信输入则不建议依赖解析器差异。Go 的 JSON v2 方向已经提供拒绝重复名的选项和错误类型,但是否采用要看项目版本、实验特性和依赖约束;不能因为看到新 API 就直接替换生产解码器。
- 兼容场景:保留原文、记录重复键计数,并让调用方逐步修复。
- 安全场景:重复键直接返回 400 或消息进入隔离队列。
- 审计场景:保存键名与对象路径,不把敏感值写入日志。
我的判断是:只要同一字段会影响权限、金额、状态迁移或下游签名,就把重复键视为输入错误,而不是一种“最后写入获胜”的方便语法。
相关问题
JSON 重复键是合法还是非法?
JSON 标准没有规定重复成员名必须采用哪一种解释,不同实现可以拒绝、覆盖或合并。Go v1 默认接受,协议边界应自行规定。
为什么改成结构体后结果仍然是最后一个值?
因为结构体字段也按输入顺序匹配;同名字段的后一次解码会覆盖先前赋值。需要检查的是原文和字段匹配,不是 map 遍历。
只用 json.Valid 能检测重复键吗?
不能。json.Valid 只判断语法是否有效;重复键在 v1 语法层面可以被接受,需额外做成员名扫描或使用明确拒绝重复名的解码策略。
排查这类问题时,保留原文是第一步,确认目标类型是第二步,最后才决定兼容还是拒绝。这样能把“少了一个值”的现象还原成可解释、可测试的输入规则。
-
167 收藏
-
Golang · Go教程 | 2个月前 | 字符串处理 · Go教程 · 输入校验 · 字符串校验 unicode/utf8 DecodeRuneInString utf8.ValidString 非法UTF-8195 收藏
-
263 收藏
-
Golang · Go教程 | 1个月前 | Go教程 · 输入校验 · Go math/big.Int.SetString Go 大整数进制解析 SetString 返回值判断 Go 输入校验 big.Int 字符串转换334 收藏
-
232 收藏
-
196 收藏
-
307 收藏
-
353 收藏
-
308 收藏
-
222 收藏
-
Golang · Go问答 | 30分钟前 | 结构体 · api设计 · Go问答 · encoding/json · Go 结构体 dto encoding/json 未导出字段 json.Marshal MarshalJSON295 收藏
-
298 收藏
-
Golang · Go问答 | 35分钟前 | Go问答 · 接口排查 · encoding/json · JSON Go unmarshal encoding/json DisallowUnknownFields UnmarshalTypeError struct tag273 收藏
-
Golang · Go问答 | 39分钟前 | net/http · Go问答 · Go net/http http/2 ResponseController ResponseWriter Hijack115 收藏
-
368 收藏
-
Golang · Go问答 | 42分钟前 | 重定向 · net/http · HTTP客户端 · Go问答 · Go HTTP重定向 http.Client CheckRedirect 跨主机跳转445 收藏
-
Golang · Go问答 | 1小时前 | net/http · Go问答 · 接口排查 · cookie Go Cookiejar http.Client Set-Cookie AddCookie116 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习