Python 读取 JSON 的三个边界:重复键、NaN 与小数精度

举报
yd_241387476 发表于 2026/10/30 16:43:02 2026/10/30
【摘要】 用 Python 标准库在解析阶段拒绝重复键和 NaN/Infinity,并通过 Decimal 保留十进制小数。包含可运行代码、嵌套与 Unicode 转义反例,以及序列化、大小和深度限制的边界说明。

读取 JSON 时,json.loads() 没有抛出异常,只能说明这段输入被当前解析器接受。对配置校验、接口联调和数据导入工具来说,还需要约定重复字段如何处理、是否接受非标准数值,以及小数以什么类型保留。下面用 Python 标准库实现一个可以直接运行的小例子。

1. 重复键要在生成字典之前发现

例如,json.loads('{"timeout":10,"timeout":30}') 默认得到 {"timeout":30}。如果随后才遍历字典,前一个值已经丢失,无法再证明原始文本里出现过重复字段。

这里选择“发现重复键就拒绝输入”的规则。object_pairs_hook 接收到对象的有序键值对,可以在构建字典时检查。同一对象内重复才报错;两个不同对象分别包含 id 完全正常。该回调也会作用于嵌套对象,以及数组里的对象。

参考:Python json 文档:重复名称的处理:https://docs.python.org/3/library/json.html#repeated-names-within-an-object。

2. NaN 与 Infinity 需要单独拒绝

Python 的 JSON 解码器默认接受 NaN、Infinity 和 -Infinity,这三个标记并不是标准 JSON 数值。读取时可用 parse_constant 拒绝它们。不要把 allow_nan=False 用在 json.loads() 上:它是序列化时使用的选项。

另外,strict=True 主要控制字符串中的控制字符,并不会自动完成重复键检测或非标准数值拦截。

参考:Python json 文档:非有限数值:https://docs.python.org/3/library/json.html#infinite-and-nan-number-values、JSONDecoder 参数:https://docs.python.org/3/library/json.html#json.JSONDecoder。

3. 从原始数字文本构造 Decimal

如果希望保留十进制小数,可以设置 parse_float=Decimal。这样 1.10 直接从原始文本构造为 Decimal("1.10"),无需先经过二进制浮点数。整数仍沿用默认的 int,布尔值和 null 的解析也不受影响。

注意:拒绝 Infinity 字面量与限制数值范围是两件事。例如 1e400 是另一种输入;下面的实现会把它解析成有限的 Decimal,但具体业务是否允许这样大的数,仍应另外校验。

4. 完整实现

import json
from decimal import Decimal

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

def reject_constant(value):
    raise ValueError(f"non-standard JSON constant: {value}")

def load_json(text):
    return json.loads(
        text,
        object_pairs_hook=no_duplicate_keys,
        parse_constant=reject_constant,
        parse_float=Decimal,
    )

data = load_json('{"amount":1.10,"enabled":true}')
assert data["amount"] == Decimal("1.10")
assert isinstance(data["amount"], Decimal)
assert data["enabled"] is True

object_hook 接收到的已经是字典,不能用来恢复被覆盖的重复字段;若同时设置两个 hook,object_pairs_hook 优先。

5. 用反例验证约定

把下面代码接在实现后面,即可验证几类必须拒绝的输入。JSONDecodeError 也是 ValueError 的子类,因此这里同时接住语法错误与自定义校验错误。

bad_inputs = [
    '{"x":1,"x":2}',
    '{"outer":{"x":1,"x":2}}',
    '[{"x":1,"x":2}]',
    r'{"a":1,"\u0061":2}',
    "NaN",
    "Infinity",
    "-Infinity",
    '{"v":NaN}',
    '{"x":1,}',
]

for text in bad_inputs:
    try:
        load_json(text)
    except ValueError:
        pass
    else:
        raise AssertionError(f"unexpected acceptance: {text}")

assert load_json('{"a":{"x":1},"b":{"x":2}}') == {
    "a": {"x": 1}, "b": {"x": 2}
}
assert load_json("1e400").is_finite()

Unicode 转义的例子尤其容易漏掉:"a" 与 "\u0061" 解码后是同一个键,应按重复处理。验证时既要准备应被拒绝的输入,也要准备不同对象使用同名字段等合法输入,防止规则过度拦截。本文实现及相关边界在本地 Python 3.14.6 上完成了 17 项检查,均通过。

6. 接入工具时还要明确的边界

这个函数只落实了文中三项解析约定。接收不可信输入时,仍需限制请求或文件大小,按使用场景限制嵌套深度,并校验字段、类型与数值范围。深度限制应由适合的解析层落实,不能依靠完整解析后的检查来保护解析阶段。

Decimal 也不会被标准库 json.dumps() 自动序列化。不要为了消除异常就统一转成 float,否则可能重新引入精度变化;转成字符串则会改变 JSON 字段类型。应先约定输出契约,再选择匹配的序列化方式。

重复键错误目前只返回键名,不含完整父路径;如果产品需要精确定位到行列与路径,还需要更完整的解析诊断设计。日志中也应避免直接打印完整的敏感输入。

本文示例和说明使用 AI 辅助整理,并通过实际运行核对结果。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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