JSON能解析不等于配置有效:重复键、非标准数值与顶层类型
作者: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,不使用真实业务配置。
- 点赞
- 收藏
- 关注作者
评论(0)