[BUG] WriteClassName 模式下 NaN 被错误序列化为 "nullD"
- Dominant language
- Java
- Stars
- 4.4k
- Forks
- 613
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 6
Description
### 问题描述
在 Fastjson2 2.0.64 中,当开启 `JSONWriter.Feature.WriteClassName` 对包含 `NaN`、`Infinity` 等非有限 `Double` 值的对象进行序列化时,在特定字段类型下会生成无法被 Fastjson2 自身解析的非法 JSON。
例如,当字段声明为 `Object`,实际值为 `Double.NaN` 时:
```text
Double.NaN
↓
JSON.toJSONString(..., WriteClassName)
↓
{"@type":"...","value":nullD}
````
其中 `nullD` 不是合法的 JSON 值,随后使用 Fastjson2 进行反序列化时会抛出 `JSONException`。
该行为破坏了 Fastjson2 序列化与反序列化之间的一致性,并且问题发生在 `WriteClassName` 模式下对数值类型后缀 `D` 的处理过程中。
### 环境信息
* OS信息:Linux x86_64
* JDK信息:JDK 8
* Fastjson2 version: 2.0.65-SNAPSHOT
* Git commit: af56f06ee
* Git describe: 2.0.64-4-gaf56f06ee
### 重现步骤
创建如下测试代码:
```java
package org.apache.dubbo;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONWriter;
import org.junit.jupiter.api.Test;
public class Fastjson2NullDBugTest {
public static class TestBean {
public Object value;
}
@Test
public void testNaNWithWriteClassName() {
TestBean bean = new TestBean();
bean.value = Double.NaN;
String json = JSON.toJSONString(
bean,
JSONWriter.Feature.WriteClassName
);
System.out.println("生成结果:");
System.out.println(json);
TestBean bean2 = JSON.parseObject(
json,
TestBean.class
);
System.out.println("解析结果:");
System.out.println(bean2.value);
}
}
```
执行:
```bash
./mvnw -pl core -Dtest=org.apache.dubbo.Fastjson2NullDBugTest test
```
实际生成:
```json
{"@type":"org.apache.dubbo.Fastjson2NullDBugTest$TestBean","value":nullD}
```
随后反序列化失败:
```text
com.alibaba.fastjson2.JSONException:
illegal fieldName input68, offset 72, character D, line 1, column 72,
fastjson-version 2.0.64
{"@type":"org.apache.dubbo.Fastjson2NullDBugTest$TestBean","value":nullD}
```
完整异常:
```text
com.alibaba.fastjson2.JSONException: illegal fieldName input68, offset 72, character D, line 1, column 72, fastjson-version 2.0.64 {"@type":"org.apache.dubbo.Fastjson2NullDBugTest$TestBean","value":nullD}
at com.alibaba.fastjson2.JSONReader.readFieldNameHashCodeError(JSONReader.java:6874)
at com.alibaba.fastjson2.JSONReaderUTF16.readFieldNameHashCodeE(JSONReaderUTF16.java:1265)
at org.apache.dubbo.Fastjson2NullDBugTest.testNaNWithWriteClassName(Fastjson2NullDBugTest.java:25)
```
### 预期结果
序列化结果应该是合法的 JSON,例如:
```json
{"@type":"org.apache.dubbo.Fastjson2NullDBugTest$TestBean","value":null}
```
或者使用其他合法的 JSON 表示方式表示 `NaN`。
至少不应该生成:
```text
nullD
```
因为该结果无法被 Fastjson2 自身正常反序列化。
### 根因分析
问题出现在:
```text
core/src/main/java/com/alibaba/fastjson2/writer/ObjectWriterImplDouble.java
```
相关代码:
```java
double value = (Double) object;
if ((features & JSONWriter.Feature.WriteNonStringValueAsString.mask) != 0) {
jsonWriter.writeString(value);
return;
}
jsonWriter.writeDouble(value);
long features2 = jsonWriter.getFeatures(features);
if ((features2 & JSONWriter.Feature.WriteClassName.mask) != 0
&& (features2 & JSONWriter.Feature.WriteNonStringKeyAsString.mask) == 0
&& (features2 & JSONWriter.Feature.NotWriteNumberClassName.mask) == 0
&& fieldType != Double.class && fieldType != double.class) {
jsonWriter.writeRaw('D');
}
```
对于:
```java
bean.value = Double.NaN;
```
`writeDouble(value)` 会将非有限 `double` 值写成:
```text
null
```
其中:
```java
if (Double.isNaN(doubleValue)
|| doubleValue == Double.POSITIVE_INFINITY
|| doubleValue == Double.NEGATIVE_INFINITY) {
return writeSpecial(buf, off, (float) doubleValue, json, writeSpecialAsString);
}
```
但是,在 `WriteClassName` 开启时,`ObjectWriterImplDouble` 后续仍然根据字段类型判断是否需要追加 `D` 后缀。
本例中的字段声明为:
```java
public Object value;
```
因此:
```java
fieldType == Object.class
```
满足:
```java
fieldType != Double.class
&& fieldType != double.class
```
最终执行:
```java
jsonWriter.writeRaw('D');
```
导致:
```text
null + D
```
最终生成:
```text
nullD
```
### 影响
该问题会导致:
1. `WriteClassName` 模式下产生非法 JSON;
2. 序列化后的结果无法被 Fastjson2 自身反序列化;
3. 可能破坏依赖 `toJSONString()` → `parseObject()` 数据流的应用;
4. `NaN`、`Infinity`、`-Infinity` 等非有限浮点值均可能受到影响。
### 相关代码
```text
core/src/main/java/com/alibaba/fastjson2/writer/ObjectWriterImplDouble.java
core/src/main/java/com/alibaba/fastjson2/util/NumberUtils.java
```
### 结论
`JSONWriter.Feature.WriteClassName` 对数值类型后缀的处理没有正确考虑 `NaN`、`Infinity` 和 `-Infinity` 已被序列化为 `null` 的情况。
建议在追加 `D` 后缀之前判断当前数值是否为非有限值,避免对已经序列化为 `null` 的值继续追加类型后缀,从而生成非法的 `nullD`。
Contributor guide
Research direction
Start with core/src/main/java/com/alibaba/fastjson2/writer/ObjectWriterImplDouble.java and reproduce the issue with ./mvnw -pl core -Dtest=org.apache.dubbo.Fastjson2NullDBugTest test. Check the interaction between writeDouble and the WriteClassName suffix handling for NaN and infinities. Done means serialization no longer produces nullD and the resulting value can be parsed successfully by Fastjson2.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100