侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 229 篇文章
  • 累计收到 7 条评论

JSON 对象重复键,Python 默认留下哪一个?

2026-10-22 / 0 评论 / 7 阅读

JSON 对象重复键,Python 默认留下哪一个?

{"role":"user","role":"admin"}

同一个 JSON 对象里写了两个 role,json.loads 会留下哪个?在 Python 里默认留下后一个,结果直接成了 {'role': 'admin'}。语法看上去像一个普通对象,重复键却已经被折叠了。

这算不算非法 JSON?

RFC 8259 对对象成员名的建议是应该唯一;遇到重复名称时,各实现的行为不可预测。有的实现保留最后一个,有的报错,也有的保留多个。输入在系统间转手时,差异就可能变成权限、配置或签名校验的问题。

Python 标准库采取的是「最后一个值覆盖前面值」的做法。通常业务数据没重复键,没什么感觉。可若 JSON 来自不可信请求,网关和应用对同一串文本得出不同对象,后面的判断就不再基于同一份数据。

这里不是说每个 JSON 接口都要把解析改成严格模式。关键是协议边界要明确:某个字段重复出现到底是错误,还是有既定的合并规则。没有约定时,静默覆盖最难排查,因为解析结果看上去完全正常。

默认解析会怎么处理?

在本机 Python 3.9.6 上,两个相同键被压成一个。下面的输出来自脚本 tools/verify-json-duplicate-keys-20261022.py,原始日志在同目录的 .log:

top-level: default={'role': 'admin'}; strict="ERROR duplicate key: 'role'"
nested: default={'account': {'role': 'admin'}}; strict="ERROR duplicate key: 'role'"
distinct: default={'role': 'user', 'enabled': True}; strict={'role': 'user', 'enabled': True}

顶层和嵌套对象都一样。strict 不是标准库内置的严格模式,只是给 json.loads 传入 object_pairs_hook,在键值对还没变成字典时检查一次。函数收到的是一串 (key, value),重复的第二次出现仍然看得见。

object_pairs_hook 为什么能抓到?

默认解码器会把对象成员转换成 dict。重复键在这一步被覆盖;等 json.loads 返回,早先的值已经丢了。object_pairs_hook 则让调用方接手对象成员列表,适合在映射形成之前做重复检查:

import json

def reject_duplicates(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError(f"duplicate key: {key!r}")
        result[key] = value
    return result

payload = '{"role":"user","role":"admin"}'
value = json.loads(payload, object_pairs_hook=reject_duplicates)

这段会抛出 ValueError。真实接口里要把它转成明确的客户端错误,例如 HTTP 400;不要把解析异常当成服务器内部故障,返回一个含糊的 500。错误响应里也没必要回显整份原始输入,避免把敏感字段抄进日志或响应。

这个检查只针对同一个对象内重复出现的键。{"role":"admin","account":{"role":"user"}} 是合法的两个不同对象,钩子不会把它们误认成冲突。判重的范围跟对象边界一致,不是整份 JSON 共用一张键名表。

什么时候不该直接拒绝?

如果上游格式明确规定重复键有意义,或者系统必须兼容某个已知客户端,可以另行定义规则。要紧的是所有参与校验和消费数据的组件遵守同一规则。只在一层拒绝、另一层仍按「后值覆盖」处理,问题只是换了位置。

这个钩子解决的是 Python 解码时的重复成员,不会替接口做完整的 schema 校验。字段是否必填、类型是否正确等约束仍需单独检查;额外字段也应按接口规则处理。把重复键拦住,不代表解析出的对象已经可信。遇到签名或权限判断,边界条件也该由调用方的测试覆盖,而不是只确认解析成功。

签名场景尤其别把解析后的字典想当然地当成原始 JSON。签名若覆盖原始字节,就按原始字节验签;若覆盖规范化后的对象,规范化算法必须明确重复键如何处理。否则一端签的是前一个值,另一端用的却是后一个值,代码两边都能跑,含义已经分叉。

我会在接受外部 JSON 的边界拒绝重复键,除非协议明确要求另一种行为。内部可信配置也可以用同一个钩子,代价不大;但对已经明确允许重复字段的遗留格式,不要擅自改变解析结果。

    🤞 分享