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