Python JSON 配置加载:拒绝重复键与非标准常量

举报
yd_226410095 发表于 2026/11/05 07:23:19 2026/11/05
【摘要】 使用 Python 标准库 object_pairs_hook 拒绝嵌套重复键,使用 parse_constant 拒绝 NaN、Infinity 和 -Infinity 字面量。本文给出可运行实现,并明确顶层类型、科学计数法溢出和业务校验的边界,避免把局部严格解析误认为完整 JSON Schema 验证。

能被解析,不等于配置符合预期

JSON 配置看起来简单,但同名键可能隐藏错误。例如 {"timeout": 10, "timeout": 30},Python 的默认解析会保留后面的 30,而不是把重复键作为错误。多人编辑、模板生成或系统拼接配置时,静默覆盖可能让排查方向偏离真正原因。

另一个边界是 NaN、Infinity 和 -Infinity。它们不是标准 JSON 数值字面量,但 Python 的 json 模块默认允许解析。本文只收紧两个明确边界:对象重复键,以及这三个非标准常量。这里的 strict 只是辅助函数名,不代表完整模式验证,也不保证所有浮点运算结果有限。

在字典生成前检查键值对

普通对象解析成字典后,前一个同名键的值已经丢失。object_pairs_hook 接收对象的键值对列表,可以在构造字典时发现重复键。回调作用于每个 JSON 对象,因此嵌套对象同样受到检查;数组里的对象也不例外。

import json


class DuplicateKeyError(ValueError):
    """JSON 对象中出现重复键。"""


class NonFiniteNumberError(ValueError):
    """JSON 中出现 NaN 或 Infinity 字面量。"""


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


def reject_non_finite(value):
    raise NonFiniteNumberError(f"non-standard constant: {value}")


def loads_strict(text):
    """拒绝对象重复键和三个非标准数值常量,不替代业务校验。"""
    return json.loads(
        text,
        object_pairs_hook=reject_duplicates,
        parse_constant=reject_non_finite,
    )


def loads_object(text):
    """在上述解析基础上,额外要求顶层必须是对象。"""
    value = loads_strict(text)
    if not isinstance(value, dict):
        raise TypeError("top-level JSON value must be an object")
    return value


if __name__ == "__main__":
    print(loads_strict('{"service": {"port": 8080}}'))
    print(loads_strict('[1, {"enabled": true}, null]'))
    for bad in (
        '{"x": 1, "x": 2}',
        '{"metrics": {"p95": NaN}}',
        '{"limit": Infinity}',
        '{"limit": -Infinity}',
    ):
        try:
            loads_strict(bad)
        except (DuplicateKeyError, NonFiniteNumberError) as exc:
            print(type(exc).__name__, exc)

键名大小写与嵌套范围

reject_duplicates 不调用 lower(),因此 "A" 和 "a" 是不同的键。如果业务希望忽略大小写,应另外规定命名和冲突策略,不能悄悄混入这一层解析。也应先拒绝重复键,再做业务字段映射,避免映射阶段的归一化掩盖冲突。

配套测试全部使用虚构的内存字符串,覆盖顶层与嵌套重复键、三种非标准常量、数组与布尔标量、仅允许顶层对象的额外约束、大小写键以及 null,共 10 项,全部通过。测试没有读取真实配置、用户数据或凭据。

parse_constant 的能力与溢出边界

parse_constant 接收解析器遇到的 NaN、Infinity、-Infinity,回调抛出异常即可拒绝它们。但它不是任意数值校验器,也不会替代整数约束、端口范围或金额精度检查。普通数字和科学计数法仍由默认数值解析逻辑处理。

一个容易忽视的区别是 1e400:它是符合 JSON 数字形式的文本,但默认转换为 Python float 时可能溢出成 inf。额外的内存测试已确认,此时 parse_constant 不会被调用。因此本例不能声称拒绝所有可能产生非有限结果的输入。若业务要求每个浮点结果都有限,应另行设计 parse_float 回调或解析后的数值检查,并增加相应边界测试。

顶层对象是业务约定,而非唯一合法 JSON

loads_strict 保留通用加载行为:对象、数组、字符串、数字、布尔值和 null 都可以是 JSON 顶层值。数组不是对象,并不意味着它违反 JSON 语法。

如果配置规范明确要求顶层为对象,使用 loads_object。它先检查重复键和三个特殊常量,再检查结果是否为 dict。此时 loads_object("[1, 2]") 抛出 TypeError,是额外的业务限制,应在接口文档中说明。不要把两类要求混为一谈。

解析之后仍要校验业务结构

上述代码不检查必填字段、枚举值、字符串长度、字段之间的依赖关系,也不处理完整 JSON Schema。工程中可以先解析并排除明确的歧义,再用业务代码或模式校验器验证结构和约束。

语法错误仍由标准的 json.JSONDecodeError 报告,例如缺逗号或字符串未闭合。调用方可以分别处理语法、重复键、特殊常量和业务校验错误,不应使用一个宽泛的 except Exception 将所有问题吞掉。错误信息也应遵守日志规范,避免附带完整敏感配置。

把严格性的范围写清楚

object_pairs_hook 提供了字典生成前的检查机会,parse_constant 则能收紧三个非标准常量的默认宽松行为。二者不依赖第三方包,也不改变合法数组和标量的顶层解析规则。可靠的配置加载不在于给函数加一个 strict 名称,而在于明确边界、配套测试,并在解析之后继续检查业务要求。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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