Skip to content

fix: JSONB ASM direct-write skips level tracking causing false 'level too large' on flat lists, for issue #3616#7635

Open
daguimu wants to merge 1 commit into
alibaba:mainfrom
daguimu:fix/asm-jsonb-level-tracking-3616
Open

fix: JSONB ASM direct-write skips level tracking causing false 'level too large' on flat lists, for issue #3616#7635
daguimu wants to merge 1 commit into
alibaba:mainfrom
daguimu:fix/asm-jsonb-level-tracking-3616

Conversation

@daguimu
Copy link
Copy Markdown

@daguimu daguimu commented Apr 29, 2026

Problem

JSONB.toBytes(list) on a flat list of JavaBeans throws JSONException: level too large : 2049 after ~2049 elements, even though the structure has no actual nesting. Reported reproducer (issue #3616):

List<TestObject> list = new ArrayList<>();
for (int i = 0; i < 2100; i++) {
    list.add(new TestObject("a" + i, new BigDecimal("1232132")));
}
JSONB.toBytes(list);  // throws "level too large : 2049"

TestObject has a String field and a BigDecimal field — both standard, both unrelated to nesting. Stack trace:

JSONException: level too large : 2049
  at JSONWriter.overflowLevel
  at JSONWriterJSONB.startObject (line 81)
  at OWG_x_x_TestObject.writeJSONB (ASM-generated)
  at ObjectWriterImplList.writeJSONB

Root cause

ObjectWriterCreatorASM.genMethodWriteJSONB partitions the bean's field writers into "direct" and "non-direct" groups. The two paths emit the BC_OBJECT / BC_OBJECT_END framing differently:

  • Non-direct path (group.start): calls JSONWriter.startObject() — writes BC_OBJECT and increments level.
  • Non-direct path (group.end): calls JSONWriter.endObject() — writes BC_OBJECT_END and decrements level.
  • Direct path (group.start): writes BC_OBJECT raw to the byte buffer — does not touch level.
  • Direct path (group.end): writes BC_OBJECT_END raw to the byte buffer — does not touch level.

For TestObject, the field groups order out as [BigDecimal c (non-direct, start=true)] + [String d (direct, end=true)]. So startObject() increments level but the matching end is the raw-byte direct path, which leaves level incremented. Each bean in the list leaks 1 level. At iteration 2049 we hit context.maxLevel and overflowLevel() fires.

The same imbalance occurs on any field-group split where start and end fall on different paths.

Fix

Mirror the level bookkeeping on the direct-write path so it stays consistent with startObject() / endObject():

  • New JSONWriter.incrementLevel() — bumps level and runs the same overflow check as startObject(). No byte output.
  • New JSONWriter.decrementLevel() — decrements level. No byte output.

ObjectWriterCreatorASM calls incrementLevel() after writing BC_OBJECT raw and decrementLevel() after writing BC_OBJECT_END raw. The direct path keeps its raw-byte field-value writes, so the performance optimisation is preserved.

Tests

core/src/test/java/com/alibaba/fastjson2/issues_3600/Issue3616.java:

  • testFlatListBigDecimalAndString — 2100-element list of (String, BigDecimal) beans; serialise to JSONB and round-trip back, asserting size and field values. Reproduces the issue exactly and verifies it is gone.
  • testFlatListWithDateObject — 2100-element list of (String, Date) beans, exercising another non-direct field type to guard against future regressions.

Existing regression tests still pass:

  • Issue3657 (intentional level too large for genuinely deep nesting) — 6 tests
  • Issue7616 (related fix for direct-write byte-frame capacity) — 3 tests
  • *Writer* test suite — 394 tests, all green

Impact

Surgical: 2 helper methods on JSONWriter (no behaviour change for existing callers) plus 6 lines of bytecode emission in the JSONB ASM generator. No JSON-text serialiser changes, no API surface broken.

Fixes #3616

…acking, for issue alibaba#3616

When a Bean's field writers are split across direct-write and non-direct-write
groups (e.g. a String field plus a BigDecimal field), the non-direct path emits
the leading BC_OBJECT via startObject() and the direct path emits the trailing
BC_OBJECT_END as a raw byte. startObject() bumps level but the raw-byte end never
decrements it, so each serialized bean leaks 1 level. After ~2049 beans the
JSONWriter throws "level too large : 2049" even though the structure has no real
nesting.

Mirror the level tracking on the direct-write path: increment level after writing
BC_OBJECT raw, decrement after writing BC_OBJECT_END raw. New incrementLevel /
decrementLevel helpers on JSONWriter expose just the level bookkeeping (no byte
output), so the direct path keeps its raw-byte performance.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] level too large : 2049

1 participant