alibaba / alibaba/fastjson2

[BUG] JSONB Map<Long/Integer,V> field loses key type on round-trip (default features)

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

Description

### 问题描述 / Description

When a field is declared `Map` (or `Map`) and the enclosing object is serialized to **JSONB** (binary format) with the default `JSONWriter` features — i.e. **without** `JSONWriter.Feature.WriteClassName` — the map's numeric keys come back as `String` instead of `Long`/`Integer` after `JSONB.parseObject(...)`. No shared/circular references or `ReferenceDetection` are involved; a single, unshared entry is enough to reproduce it.

Because the actual stored key is a `String` while the field is declared `Map`, the two realistic access patterns fail differently:
- `map.get(1L)` silently returns `null` (`Map.get(Object)` doesn't check the declared key type against the argument), leading to a downstream `NullPointerException`.
- `for (Long key : map.keySet())` throws `ClassCastException: class java.lang.String cannot be cast to class java.lang.Long`, because javac inserts an implicit `checkcast` to `Long` for the generically-typed iteration — this can crash ordinary application code with no fastjson2 frames anywhere in the stack trace, making it very hard to diagnose from the caller's side.

The equivalent **text-JSON** round-trip (`JSON.toJSONString` / `JSON.parseObject`) does **not** have this problem, and JSONB **with** `JSONWriter.Feature.WriteClassName` enabled also works correctly — so this is specific to the JSONB codec's default (no `@type`) write path.

### 环境信息 / Environment

- 版本信息 / Version: current `main` branch (also present in released versions; not yet bisected to a specific release)
- JDK: any (reproduced on JDK 17)

### 重现步骤 / Steps to reproduce

```java
import com.alibaba.fastjson2.JSONB;
import java.util.HashMap;
import java.util.Map;

public class Repro {
public static class Item {
public Long itemId;
}
public static class Bean {
public Map map1;
}

public static void main(String[] args) {
Item item = new Item();
item.itemId = 300L;

Bean bean = new Bean();
bean.map1 = new HashMap<>();
bean.map1.put(1L, item);

byte[] bytes = JSONB.toBytes(bean); // default features, no WriteClassName
Bean bean2 = JSONB.parseObject(bytes, Bean.class);

System.out.println(bean2.map1.get(1L)); // expected: Item, actual: null
for (Long key : bean2.map1.keySet()) {
System.out.println(key); // throws ClassCastException before printing anything
}
}
}
```

### 期待的正确结果 / Expected

`bean2.map1.get(1L)` returns the `Item`, and iterating `map1.keySet()` with the declared `Long` type does not throw — matching the behavior of the text-JSON round-trip.

### 实际输出 / Actual

`bean2.map1.get(1L)` returns `null`; iterating `for (Long key : bean2.map1.keySet())` throws:
```
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Long (java.lang.String and java.lang.Long are in module java.base of loader 'bootstrap')
```

### 补充说明 / Additional notes

This was found while investigating #3516 — it's a separate, unrelated bug (no reference detection or sharing involved) discovered in the same debugging session. I have a local reproduction test and a candidate fix ready; will follow up with a PR.

Contributor guide

Open the contributing guide

Research direction

Start from the supplied Repro entry point and the JSONB.toBytes/parseObject round-trip with default features. Compare it with the text-JSON path and the JSONB path using WriteClassName. Done means a regression test confirms Map and Map preserve numeric key types without class-cast failures.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.