Python JSON 配置加载:拒绝重复键与非标准常量
能被解析,不等于配置符合预期
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 名称,而在于明确边界、配套测试,并在解析之后继续检查业务要求。
- 点赞
- 收藏
- 关注作者
评论(0)