Python 严格读取 JSON 配置:重复键、NaN、布尔值与金额精度

举报
yd_226410095 发表于 2026/11/07 20:14:40 2026/11/07
【摘要】 用 Python 标准库 json 与 decimal 区分解析成功和业务合法:拒绝重复键与 NaN/Infinity,直接解析 Decimal,排除布尔值冒充整数,并说明金额字符串协议和输出序列化边界。示例已在本地运行验证。

先分清两层含义

读取配置时经常出现一个误区:把“Python 默认解析成功”当成“业务数据合法”。这其实是两层概念。json 负责把文本转换成 Python 对象,提供通用的兼容行为;业务校验则要决定哪些输入允许进入程序。默认行为为了兼容不同来源,未必符合配置、计费或风控场景的严格要求。

例如,JSON 对象中的键在规范语义上应该唯一,但 Python 默认不会因重复键报错,而是保留最后一个值。NaN、Infinity 和 -Infinity 也不属于标准 JSON 数值,Python 的解码器却默认能够识别它们。应用若需要更窄的输入范围,就应该在解析入口显式收紧规则,而不是在后续代码里猜测数据是否被覆盖或是否可参与计算。

边界一:重复键不能静默覆盖

json.loads 的 object_pairs_hook 会收到对象中的有序键值对列表。相比对象已经变成 dict 后再检查,它能在字典合并前发现重复键,因此适合实现“同名键立即失败”的策略。

下面的 reject_duplicate_keys 只做一件事:逐项加入结果字典,发现已有键就抛出 ValueError。这个策略也适用于嵌套对象,因为钩子会用于每个 JSON 对象。需要注意,是否拒绝重复键是业务选择;如果某个兼容性场景确实规定“最后一个生效”,就应把它写进接口约定,而不要让默认行为悄悄决定规则。

边界二:非标准数值要在解析时拒绝

Python 文档明确说明,解码器默认把 NaN、Infinity 和 -Infinity 转为相应的浮点值。它们在很多 JSON 互操作场景中都不是合法 JSON 数字。parse_constant 可以接收这三个文本标记;传入一个始终抛错的函数,就能把它们挡在配置对象之外。

这与 float 的有限性检查不是一回事。后者只能检查已经得到的 Python 浮点对象,不能恢复原始文本,也不能覆盖配置解析入口。若业务还要求所有数值都满足范围、单位或小数位数,解析后的字段校验仍然需要单独进行。

边界三:小数不要先落到 float

默认情况下,JSON 实数会调用 float。二进制浮点无法精确表示许多十进制小数,后续金额加法可能出现熟悉的 0.30000000000000004。parse_float=Decimal 会把 JSON 小数的文本直接交给 Decimal,避免先经过二进制浮点这一跳。

这并不意味着设置一个参数就完成了金额校验。代码还要明确币种、允许的小数位、舍入模式、上下限以及缺失字段的处理方式。Decimal 解决的是表示和运算的基础问题,业务规则仍然需要由应用定义。整数 JSON 数值通常会解析为 int;如果金额字段要求统一的十进制类型,也可以在字段校验阶段把整数转换为 Decimal。

解析参数只负责转换,不负责理解字段含义。例如 parse_float=Decimal 会影响所有实数,包括本来不应出现小数的重试次数;它不会自动把负数变成错误,也不会限制小数位。对象钩子同样只处理结构层面的重复键。完成解析后,仍应按字段逐一检查必填性、允许类型、范围和相互依赖关系。把“语法解析”和“业务校验”分成两个清晰阶段,错误信息才更容易定位,测试也更容易覆盖。

边界四:bool 不是业务上的整数

在 Python 中,bool 是 int 的子类,所以 isinstance(True, int) 的结果是 True,而 True + 1 也能参与整数运算。可是配置里的 retry、timeout 或端口号若要求整数,传入 true 往往是类型错误,而不是数字 1。

因此,严格的整数参数应使用 type(value) is int,而不是只使用 isinstance(value, int)。这是一个有意排除布尔值的类型检查。类似地,字段校验还应检查是否为非负数、是否落在允许范围内;类型正确不等于值正确。

一套可运行的严格读取代码

下面的代码只依赖标准库。load_strict 同时启用三项解析策略,require_int 展示布尔值边界,require_money 则采用“解析为 Decimal 后再接收”的约定。

import json
from decimal import Decimal


def reject_duplicate_keys(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError(f"重复键: {key}")
        result[key] = value
    return result


def reject_constant(value):
    raise ValueError(f"禁止非标准数值: {value}")


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


def require_int(value, name):
    if type(value) is not int:
        raise TypeError(f"{name} 必须是整数,不能是 {type(value).__name__}")
    return value


def require_money(value):
    if type(value) not in (int, Decimal):
        raise TypeError("金额必须是 JSON 数字并转换为 Decimal")
    return Decimal(value)


print(json.loads('{"x": 1, "x": 2}'))
print(type(load_strict('{"price": 1.10}')['price']).__name__)
print(load_strict('{"a": 0.1, "b": 0.2}')['a'] +
      load_strict('{"a": 0.1, "b": 0.2}')['b'])

实际使用时,解析入口外层还应捕获 json.JSONDecodeError、ValueError 和字段类型错误,转换成统一的配置错误信息。不要把异常吞掉后继续使用部分配置,更不要用默认值掩盖拼写错误或重复键。

金额字符串是另一种接口协议

有些接口不把金额传成 JSON 数字,而是传成字符串,例如 {"amount": "12.30"}。这样可以保留末尾零,并避免某些客户端先用浮点数处理;服务端随后用 Decimal(value) 从字符串构造金额。这是可选的 API 约定,但它与 JSON 数值类型不是一回事:前者需要字符串格式校验,后者可以配合 parse_float=Decimal 直接得到十进制对象。两种协议不能混着写,也不能只看字段名猜类型。

还要留意输出方向。标准 json.dumps 不会直接编码 Decimal,会抛出 TypeError。如果协议规定金额输出为字符串,可以显式转换后再编码:

payload = {"amount": str(Decimal("12.30"))}
print(json.dumps(payload))  # {"amount": "12.30"}

如果协议规定输出 JSON 数字,就必须另行约定格式化、舍入和编码方式;把 Decimal 随意转成 float 会重新引入二进制浮点问题。读取严格、计算精确、输出可互操作,三件事都应写进接口契约。

本地测试结果

脚本在本地使用 Python 标准库运行成功,覆盖默认重复键行为、Decimal 解析、非标准数值、布尔整数参数、金额类型和 Decimal 序列化错误。实际输出如下:

default_duplicate: {'x': 2}
strict_float_type: Decimal
money_sum_decimal: 0.3
bool_is_int: True False
dump_decimal_error: TypeError Object of type Decimal is not JSON serializable
reject_duplicate: ValueError 重复键: x
reject_constant: ValueError 禁止非标准数值: NaN
reject_int True : TypeError retry 必须是整数,不能是 bool
require_int 3 -> 3
reject_money '12.30' : TypeError 金额必须是 JSON 数字并转换为 Decimal
require_money Decimal('12.30') -> 12.30

这组结果只证明示例在本地解释器中的行为,不代表已经在云上、数据库或任何具体业务系统中测试。配置解析的关键不是追求“什么都能读”,而是先定义可接受的输入,再让解析器尽早、明确地拒绝边界之外的数据。

上线前还应把这些边界写成固定测试:重复键必须失败,三个特殊常量必须失败,布尔值不能通过整数参数校验,金额计算不能先转浮点。这样升级 Python 版本或更换调用方时,协议约束不会只停留在口头约定中。

参考文档

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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