如何在 JSON 反序列化中无损转换大浮点数为 BigDecimal

json 中的高精度数字(如 "123345555789123495.38")若经 double 中间解析会丢失精度;正确做法是**跳过 double 类型,直接从原始字符串构造 bigdecimal**。

在 Java 处理 JSON 数据时,若后端将 JSON 数字字段(如 "amount": 123345555789123495.38)反序列化为 double 或 Double 类型,会导致严重精度丢失——因为 double 仅能精确表示约 15–17 位十进制有效数字,而示例中的整数部分已达 17 位(123345555789123495),超出 double 的安全整数范围(Long.MAX_VALUE ≈ 9.2×10¹⁸,但精度限制更早发生)。此时 1233455557891

23495.38 被转为 double 后实际存储为 1.23345555789123488E17,即 123345555789123488.0,已丢失末尾 7 个单位的整数精度。

根本解决方法是:避免经过 double 解析环节,直接获取 JSON 中该字段的原始字符串表示,再用 BigDecimal(String) 构造器初始化。BigDecimal 的字符串构造器可完全保留任意长度的十进制数字精度。

以 Jackson 为例,推荐以下实践方式:

✅ 正确做法(推荐):

// 假设使用 Jackson,且已通过 JsonNode 或 ObjectMapper 获取原始值
JsonNode node = objectMapper.readTree(jsonString);
String amountStr = node.path("amount").asText(); // 确保 asText() —— 不调用 asDouble()
BigDecimal amount = new BigDecimal(amountStr).setScale(2, RoundingMode.HALF_UP);

✅ 若必须兼容现有 ConcurrentHashMap 结构(如 extensionMap),需先判断值类型:

Object rawValue = extensionMap.get("amount");
BigDecimal amount;
if (rawValue instanceof String) {
    amount = new BigDecimal((String) rawValue).setScale(2, RoundingMode.HALF_UP);
} else if (rawValue instanceof Number) {
    // ⚠️ 警告:此处 Number 可能已是 double/Long,精度已失!应尽量避免进入此分支
    amount = BigDecimal.valueOf(((Number) rawValue).doubleValue()) // ❌ 不推荐!
        .setScale(2, RoundingMode.HALF_UP);
} else {
    throw new IllegalArgumentException("Unsupported amount type: " + rawValue.getClass());
}

⚠️ 注意事项:

  • 永远不要用 new BigDecimal(double):该构造器会继承 double 的二进制浮点误差,例如 new BigDecimal(123345555789123495.38) 实际传入的是近似值。
  • 使用 BigDecimal(String) 是唯一可靠方式,它按字面量精确解析。
  • 在 Jackson 配置中,可全局禁用 double 自动转换,强制数字字段走字符串路径(如注册自定义 JsonDeserializer)。
  • 若前端无法保证数字始终以字符串形式发送(如 {"amount": "123345555789123495.38"}),则需服务端统一约定:关键金额字段必须为 JSON 字符串类型,这是金融级系统常见规范。

总结:精度保障始于数据源头。只要 JSON 中的数值以字符串形式传递,并在 Java 层跳过任何 double 解析步骤,即可实现任意长度小数的无损 BigDecimal 转换。