JSON能解析不等于配置有效:重复键、非标准数值与顶层类型

举报
yd_234095983 发表于 2026/10/29 08:26:39 2026/10/29
【摘要】 用32项本地断言区分JSON默认解析与配置契约,检查重复键、NaN/Infinity、顶层对象和Decimal数值边界。全部合成数据,AI辅助整理。

作者:lgbisha。本文由AI辅助起草和整理验证,全部输入为合成数据。示例在本地Python 3.12.3运行,32项断言通过、0失败;没有真实业务配置、云部署或生产事故声明。

导入一份配置时,json.loads()没有抛异常,是否就可以直接应用?这次从三个小反例出发:同一个对象里写了两次同名成员、数值写成NaN、顶层给了数组。它们不应被简单混为“JSON文件坏了”,因为解析器默认行为与应用配置契约是两件事。

1. 四个默认行为,先运行再判断

本地验证的第一个输入是:

import json

result = json.loads('{"retry": 1, "retry": 9}')
assert result == {"retry": 9}

结果保留后一个值。即使这两个数字都在允许范围内,读配置的人仍然可能误以为前一个生效了。若先生成普通字典再查重复,前一个键值对已经丢失,无法从最终字典反推源文本是否重名。

另外三项本地对照是:默认解析接受NaN;1e400默认转为浮点数时得到无穷值;仅设置strict=True并不会让重复键示例被拒绝。这里的strict参数不是“开启一切配置校验”的总开关。

因此,本例不把“没抛解析异常”作为配置可应用的唯一标准,而是先明确额外契约。

2. 给这个演示加载器四条规则

  • 同一对象内不允许重复成员名,嵌套对象也一样。
  • 拒绝NaN、Infinity和-Infinity这三个非标准数值标记。
  • 配置顶层必须是对象;对象内部仍可包含数组、布尔值或null。
  • JSON中带小数点或指数形式的数值交给Decimal,整数仍由默认整数解析处理。

这些规则是本例的选择,不是宣称JSON顶层数组一律非法,也不是宣称所有系统都应该接受任意大小的Decimal。是否允许空对象、哪些字段必填,以及金额上限,都属于后续业务规则。

3. 在转换成字典之前检查重复

from decimal import Decimal
import json


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError(f"重复的对象成员名:{key!r}")
        result[key] = value
    return result


def reject_constant(token):
    raise ValueError(f"不接受非标准数值:{token}")


def load_config(text):
    result = json.loads(
        text,
        object_pairs_hook=unique_object,
        parse_constant=reject_constant,
        parse_float=Decimal,
    )
    if not isinstance(result, dict):
        raise ValueError("配置顶层必须是对象")
    return result

object_pairs_hook收到对象的成员对,适合在收敛为普通字典前检查重名;parse_constant负责上述三个标记;parse_float接收JSON浮点形式的原始数字文本。本例选择Decimal,不先经过二进制浮点数再转换。

这段加载器只接收已经解码的JSON文本。文件编码识别、上传大小限制和错误提示UI没有被偷偷包含在函数承诺里。

4. 两个容易漏掉的边界

第一,键名应该按解析后的字符串比较,而不是简单对原始文本做搜索:

text = r'{"id": 1, "\u0069d": 2}'

其中第二个成员名解码后同样是id,本例会拒绝。另一方面,下面的同名字段属于两个不同对象,不应该因为全文件出现过两次id就被误拒绝:

{"left": {"id": 1}, "right": {"id": 2}}

第二,禁止NaN标记并不代表已经限制数字量级。1e400是指数形式数字,默认float会溢出为无穷值;本例解析为Decimal("1e400"),它仍是有限的,但很可能超出某个业务字段的允许范围。Decimal改变的是表示方式,不会替你决定可接受上限。

如果下游又把这个Decimal强制转成float,仍需重新检查其范围和表示结果,不能把入口的类型选择当作永远不会溢出的保证。

5. 本次32项验证具体覆盖什么

本地验证分为四组:

  • 4项默认对照:重复键保留后值、NaN、超大指数浮点溢出,以及strict=True不拒绝重复键。
  • 10项正常输入:空对象、整数、小数、嵌套对象、布尔和null、中文成员名、不同对象的同名字段、对象内数组、普通字符串NaN、较大的有限Decimal。
  • 16项拒绝输入:顶层及嵌套重名、数组内对象重名、转义后重名、三个非标准数值标记、五种非对象顶层,以及尾逗号、多文档串联、空文本和单引号成员。
  • 2项补充检查:尾部合法空白,以及整数与Decimal实际类型。

结果是32通过、0失败。这只是上述输入与本例契约的对应结果,不是对任意JSON方言、恶意深度或生产配置系统的完整认证。

6. 下一层必须单独设计

加载成功之后,仍需检查必填字段、未知字段、枚举、范围和字段间约束。例如retry是不是整数、能否为负数、是否设置上限,都不由这段代码决定。Python中布尔值与整数有继承关系,业务若要求严格整数,也不能只凭一个宽泛的类型判断就结束。

此外,这个函数没有实现输入字节数、层级深度、数组长度和处理时限限制;没有把返回对象写入文件或数据库;也没有承诺应用新配置失败时可以自动回滚。生产导入链路还应把“解析”“校验”“预览”“应用”和“失败恢复”分开,避免边读边改造成半份配置生效。

错误提示也应控制数据范围。本例异常会显示重复的成员名,实际日志若涉及敏感配置,应进一步限制可记录的字段信息,而不是把整份源文件打印出来。

结语与来源

本次最有价值的并不是给json.loads()多加几个参数,而是把默认解析行为、应用契约和业务约束分清楚。重复键检查发生在信息丢失之前,非标准标记有明确拒绝路径,顶层类型有显式约束;剩下未实现的资源与业务规则,也清楚保留为边界。

参考Python 3.12标准库官方文档的json模块:Standard Compliance and Interoperability、Repeated Names Within an Object,以及object_pairs_hook、parse_constant、parse_float参数说明。复现仅使用标准库、合成文本与本地Python 3.12.3,不调用云API,不使用真实业务配置。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。