alibaba / alibaba/fastjson2

[BUG] 等价 JSON 数值转 int 时产生拒绝/回绕/饱和三种结果,与 JLS 窄化语义不一致

Open
#7,801 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
Java
Stars
4.4k
Forks
613
Avg merge
1d 22h
Merged PRs (30d)
6

Description

### 问题描述

同一个数学值 `10¹⁰` 绑定到 Java `int` 字段时,fastjson2 的结果取决于 JSON 数值的**词法书写形式**,而不是数值本身:

- 整数写法 `10000000000`:抛出 `JSONException`;
- 普通小数写法 `10000000000.0`:得到 `1410065408`,发生模 `2^32` 回绕;
- 科学计数法 `1e10`:得到 `2147483647`,发生饱和。

这三种 JSON token 表示完全相同的数学值,却触发了三条不同的内部转换路径。

其中科学计数法的结果与 JLS §5.1.3 规定的浮点数到整数窄化转换一致:超出 `int` 正向范围时应饱和到 `Integer.MAX_VALUE`,即:

```java
(int) 1e10 == Integer.MAX_VALUE;
```

普通小数路径表现得像 `BigDecimal.intValue()`,而整数路径执行了显式溢出拒绝。当前行为把内部中间类型和转换实现泄漏到了公开 JSON 绑定结果中,导致等价数值无法获得等价结果。

### 环境信息
- OS信息:Ubuntu 20.04.6 LTS
- JDK信息:Eclipse Temurin 1.8.0_402
- 版本信息:Fastjson2 2.0.64;2.0.65-SNAPSHOT

### 重现步骤
1. 使用 `JSON.parseObject(String, Class)` 将 JSON 对象绑定到包含 `int` 字段的 Bean。
2. 分别输入数学值相同、书写形式不同的 `10000000000`、`10000000000.0` 和 `1e10`。
3. 可以观察到拒绝、回绕和饱和三种不同结果。

```java
import com.alibaba.fastjson2.JSON;

public class Repro {
public static class Bean {
private int count;

public int getCount() {
return count;
}

public void setCount(int count) {
this.count = count;
}
}

public static void main(String[] args) {
for (String value : new String[]{"10000000000", "10000000000.0", "1e10"}) {
try {
Bean bean = JSON.parseObject("{\"count\":" + value + "}", Bean.class);
System.out.println(value + " -> " + bean.getCount());
} catch (Exception error) {
System.out.println(value + " -> " + error.getClass().getSimpleName());
}
}
}
}
```

完整写法矩阵如下,所有输入值均为 `10¹⁰`:

| 写法 | 实际结果 | 转换表现 |
|---|---:|---|
| `10000000000` | `JSONException` | 拒绝 |
| `10000000000.0` | `1410065408` | 回绕 |
| `10000000000.00` | `1410065408` | 回绕 |
| `1e10` | `2147483647` | JLS 饱和 |
| `1E10` | `2147483647` | JLS 饱和 |
| `1.0e10` | `2147483647` | JLS 饱和 |
| `100e8` | `2147483647` | JLS 饱和 |
| `0.1e11` | `2147483647` | JLS 饱和 |

### 期待的正确结果

在相同的 `JSONReader` 配置下,表示同一数学值的不同合法 JSON 写法应产生相同结果,不应因为解析阶段选择了 `Long`、`BigDecimal` 或 `Double` 中间类型而分别出现拒绝、回绕和饱和。

如果按 Java 原始类型窄化语义处理,JLS §5.1.3 给出的参考结果是饱和到 `Integer.MAX_VALUE`。如果 fastjson2 选择维持其已记录的默认溢出策略,则应至少保证:

- 默认配置下,所有超出 `int` 范围的等价写法统一抛出 `JSONException`;
- 启用 `JSONReader.Feature.NonErrorOnNumberOverflow` 后,所有等价写法统一采用相同的非报错窄化结果。

无论选择哪种策略,都不应由数值的词法书写形式决定结果。

### 相关日志输出
```text
10000000000 -> JSONException
10000000000.0 -> 1410065408
1e10 -> 2147483647
```

参考 Java 转换结果:

```java
(int) 1e10 // 2147483647,JLS §5.1.3 饱和
new BigDecimal("1e10").intValue() // 1410065408,模 2^32 回绕
```

#### 附加信息

- `long` 字段不受影响,上述八种写法均得到 `10000000000L`。
- 问题在 `String`、`char[]` 和 UTF-8 `byte[]` 输入路径均可观察到。
- 这可能造成校验层和执行层在数值文本被重新格式化后,对同一个业务值产生不同结果。

Contributor guide

Open the contributing guide

Research direction

Start at JSON.parseObject(String, Class) when binding numeric JSON tokens to an int field, then compare the integer, decimal, and exponent conversion paths described in the reproduction matrix. Check how JSONReader.Feature.NonErrorOnNumberOverflow affects each path. Done means equivalent representations of the same out-of-range value follow one consistent overflow policy, with regression coverage for the listed forms.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.