Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions core/src/main/java/com/alibaba/fastjson2/JSONWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -5022,6 +5022,28 @@ protected final void overflowLevel() {
throw new JSONException("level too large : " + level);
}

/**
* Increments the nesting level and verifies it does not exceed {@code context.maxLevel}.
* Used by code paths that emit BC_OBJECT directly to the byte buffer instead of going
* through {@link #startObject()}, so that nesting depth is still tracked correctly.
*
* @throws JSONException if the level exceeds the configured maximum
*/
public final void incrementLevel() {
if (++level > context.maxLevel) {
overflowLevel();
}
}

/**
* Decrements the nesting level. Used by code paths that emit BC_OBJECT_END directly
* to the byte buffer instead of going through {@link #endObject()}, so that nesting
* depth is still tracked correctly.
*/
public final void decrementLevel() {
level--;
}

/**
* Gets the current offset in the internal buffer.
* The offset represents the position where the next character will be written.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,9 @@ private void genMethodWriteJSONB(
// bytes[off++] = BC_OBJECT;
gwWriteByte(mw, BYTES, OFFSET, BC_OBJECT);
mw.visitIincInsn(OFFSET, 1);
// jsonWriter.incrementLevel(); — keep nesting depth in sync with startObject()
mw.aload(JSON_WRITER);
mw.invokevirtual(TYPE_JSON_WRITER, "incrementLevel", "()V");
}
for (FieldWriterRecord item : group.fieldWriters) {
writeFieldValueDirectJSONB(
Expand All @@ -916,6 +919,9 @@ private void genMethodWriteJSONB(

if (group.end) {
gwWriteByte(mw, BYTES, OFFSET, BC_OBJECT_END);
// jsonWriter.decrementLevel(); — keep nesting depth in sync with endObject()
mw.aload(JSON_WRITER);
mw.invokevirtual(TYPE_JSON_WRITER, "decrementLevel", "()V");
}

mw.aload(JSON_WRITER);
Expand Down
108 changes: 108 additions & 0 deletions core/src/test/java/com/alibaba/fastjson2/issues_3600/Issue3616.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.alibaba.fastjson2.issues_3600;

import com.alibaba.fastjson2.JSONB;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;

@Tag("regression")
public class Issue3616 {
/**
* Issue #3616: a flat list of JavaBeans whose field groups split between the
* direct-write path (e.g. String) and the non-direct-write path (e.g. BigDecimal)
* caused {@code JSONException: level too large} after ~2049 elements, even though
* the structure has no real nesting. Each bean called {@code startObject()}
* (level++) but the matching object end was emitted as a raw byte on the direct
* path, so {@code level} never decremented.
*/
@Test
public void testFlatListBigDecimalAndString() {
List<MixedBean> list = new ArrayList<>();
for (int i = 0; i < 2100; i++) {
list.add(new MixedBean("a" + i, new BigDecimal("1232132")));
}

byte[] bytes = assertDoesNotThrow(() -> JSONB.toBytes(list));
List<MixedBean> parsed = JSONB.parseArray(bytes, MixedBean.class);
assertEquals(list.size(), parsed.size());
assertEquals("a0", parsed.get(0).getD());
assertEquals(new BigDecimal("1232132"), parsed.get(0).getC());
assertEquals("a2099", parsed.get(2099).getD());
}

@Test
public void testFlatListWithDateObject() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] testFlatListWithDateObject only asserts that serialization does not throw; it never deserializes or verifies field content, unlike testFlatListBigDecimalAndString which does a full round-trip. — Concrete cost: a regression that produces structurally valid-but-wrong JSONB bytes for the Date+String split would pass this test silently.

Suggested change
public void testFlatListWithDateObject() {
public void testFlatListWithDateObject() {
List<DateBean> list = new ArrayList<>();
for (int i = 0; i < 2100; i++) {
list.add(new DateBean("name" + i, new java.util.Date(0)));
}
byte[] bytes = assertDoesNotThrow(() -> JSONB.toBytes(list));
List<DateBean> parsed = JSONB.parseArray(bytes, DateBean.class);
assertEquals(list.size(), parsed.size());
assertEquals("name0", parsed.get(0).getName());
assertEquals(new java.util.Date(0), parsed.get(0).getTime());
}

— qwen3-coder via Qwen Code /review

List<DateBean> list = new ArrayList<>();
for (int i = 0; i < 2100; i++) {
list.add(new DateBean("name" + i, new java.util.Date(0)));
}
assertDoesNotThrow(() -> JSONB.toBytes(list));
}

public static class MixedBean implements Serializable {
private static final long serialVersionUID = 1L;
private String d;
private BigDecimal c;

public MixedBean() {
}

public MixedBean(String d, BigDecimal c) {
this.d = d;
this.c = c;
}

public String getD() {
return d;
}

public void setD(String d) {
this.d = d;
}

public BigDecimal getC() {
return c;
}

public void setC(BigDecimal c) {
this.c = c;
}
}

public static class DateBean implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private java.util.Date time;

public DateBean() {
}

public DateBean(String name, java.util.Date time) {
this.name = name;
this.time = time;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public java.util.Date getTime() {
return time;
}

public void setTime(java.util.Date time) {
this.time = time;
}
}
}