Repository: alibaba/fastjson2
Pull request #3406 (2025-03-17 01:02:10)

optimization for jsonb deserialize non-default-constructor bean
### What this PR does / why we need it?



### Summary of your change



#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.



Patch:
diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderInt16Param.java b/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderInt16Param.java
index e4c1d8737..2fe319b21 100644
--- a/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderInt16Param.java
+++ b/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderInt16Param.java
@@ -1,5 +1,6 @@
 package com.alibaba.fastjson2.reader;
 
+import com.alibaba.fastjson2.JSONException;
 import com.alibaba.fastjson2.JSONReader;
 import com.alibaba.fastjson2.schema.JSONSchema;
 
@@ -35,7 +36,12 @@ final class FieldReaderInt16Param<T>
 
     @Override
     public Object readFieldValue(JSONReader jsonReader) {
-        Integer integer = jsonReader.readInt32();
-        return integer == null ? null : integer.shortValue();
+        Integer value = jsonReader.readInt32();
+        if (value == null
+                && fieldClass == byte.class
+                && (jsonReader.features(this.features) & JSONReader.Feature.ErrorOnNullForPrimitives.mask) != 0) {
+            throw new JSONException(jsonReader.info("short value not support input null"));
+        }
+        return value == null ? null : value.shortValue();
     }
 }
diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderInt32Param.java b/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderInt32Param.java
index a2aa783d7..40d37afbb 100644
--- a/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderInt32Param.java
+++ b/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderInt32Param.java
@@ -1,5 +1,6 @@
 package com.alibaba.fastjson2.reader;
 
+import com.alibaba.fastjson2.JSONException;
 import com.alibaba.fastjson2.JSONReader;
 import com.alibaba.fastjson2.schema.JSONSchema;
 
@@ -24,6 +25,12 @@ final class FieldReaderInt32Param<T>
 
     @Override
     public Object readFieldValue(JSONReader jsonReader) {
-        return jsonReader.readInt32();
+        Integer value = jsonReader.readInt32();
+        if (value == null
+                && fieldClass == int.class
+                && (jsonReader.features(this.features) & JSONReader.Feature.ErrorOnNullForPrimitives.mask) != 0) {
+            throw new JSONException(jsonReader.info("int value not support input null"));
+        }
+        return value;
     }
 }
diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderInt64Param.java b/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderInt64Param.java
index 9dc8a400c..d63095e81 100644
--- a/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderInt64Param.java
+++ b/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderInt64Param.java
@@ -1,5 +1,6 @@
 package com.alibaba.fastjson2.reader;
 
+import com.alibaba.fastjson2.JSONException;
 import com.alibaba.fastjson2.JSONReader;
 import com.alibaba.fastjson2.schema.JSONSchema;
 
@@ -25,6 +26,12 @@ final class FieldReaderInt64Param<T>
 
     @Override
     public Object readFieldValue(JSONReader jsonReader) {
-        return jsonReader.readInt64();
+        Long value = jsonReader.readInt64();
+        if (value == null
+                && fieldClass == long.class
+                && (jsonReader.features(this.features) & JSONReader.Feature.ErrorOnNullForPrimitives.mask) != 0) {
+            throw new JSONException(jsonReader.info("long value not support input null"));
+        }
+        return value;
     }
 }
diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderObject.java b/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderObject.java
index 533b01d96..ba79f51a6 100644
--- a/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderObject.java
+++ b/core/src/main/java/com/alibaba/fastjson2/reader/FieldReaderObject.java
@@ -201,6 +201,12 @@ public class FieldReaderObject<T>
         }
 
         Object value = initReader.readJSONBObject(jsonReader, fieldType, fieldName, features);
+        if (value == null
+                && (jsonReader.features(this.features) & JSONReader.Feature.ErrorOnNullForPrimitives.mask) != 0
+                && fieldClass.isPrimitive()
+        ) {
+            throw new JSONException(jsonReader.info("primitive value not support input null"));
+        }
         accept(object, value);
     }
 
diff --git a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java
index ed1916cee..6299e13f8 100644
--- a/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java
+++ b/core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreatorASM.java
@@ -454,6 +454,10 @@ public class ObjectReaderCreatorASM
 
         genMethodReadObject(context, beanInfo.readerFeatures);
 
+        if (!context.disableJSONB()) {
+            genMethodReadJSONBObject(context, beanInfo.readerFeatures);
+        }
+
         byte[] code = cw.toByteArray();
         try {
             Class<?> readerClass = classLoader.defineClassPublic(context.classNameFull, code, 0, code.length);
@@ -1028,14 +1032,14 @@ public class ObjectReaderCreatorASM
         }
 
         if (!disableArrayMapping) {
-            Label object_ = new Label();
+            Label L0 = new Label();
 
             // if (jsonReader.isArray() && jsonReader.isSupportBeanArray()) {
             {
                 Label startArray_ = new Label(), endArray_ = new Label();
                 mw.aload(JSON_READER);
                 mw.invokevirtual(TYPE_JSON_READER, "isArray", "()Z");
-                mw.ifeq(object_);
+                mw.ifeq(L0);
 
                 mw.aload(JSON_READER);
                 mw.invokevirtual(TYPE_JSON_READER, "isSupportBeanArray", "()Z");
@@ -1052,40 +1056,99 @@ public class ObjectReaderCreatorASM
                 mw.visitLabel(endArray_);
             }
 
-            mw.visitLabel(object_);
+            mw.visitLabel(L0);
         }
 
-        genCreateObject(mw, context, classNameType);
-        mw.astore(OBJECT);
+        if (context.objectReaderAdapter instanceof ObjectReaderNoneDefaultConstructor) {
+            {
+                /*
+                 * if (jsonReader.hasAutoTypeBeforeHandler()
+                 *      || (features & (JSONReader.Feature.SupportSmartMatch.mask | JSONReader.Feature.SupportAutoType.mask)) != 0
+                 * ) {
+                 *     return super.readObject(jsonReader, fieldType, fieldName, features);
+                 * }
+                 */
+                Label L3 = new Label(), L4 = new Label();
+                mw.aload(JSON_READER);
+                mw.invokevirtual(TYPE_JSON_READER, "hasAutoTypeBeforeHandler", "()Z");
+                mw.ifne(L3);
+
+                mw.lload(FEATURES);
+                mw.visitLdcInsn(JSONReader.Feature.SupportSmartMatch.mask | JSONReader.Feature.SupportAutoType.mask);
+                mw.land();
+                mw.lconst_0();
+                mw.lcmp();
+                mw.ifeq(L4);
+
+                mw.visitLabel(L3);
+                mw.aload(THIS);
+                mw.aload(JSON_READER);
+                mw.aload(FIELD_TYPE);
+                mw.aload(FIELD_NAME);
+                mw.lload(FEATURES);
+                mw.invokespecial(TYPE_OBJECT_READER_NONE_DEFAULT_CONSTRUCTOR, "readJSONBObject", METHOD_DESC_READ_OBJECT);
+                mw.areturn();
+
+                mw.visitLabel(L4);
+            }
+
+            for (FieldReader fieldReader : fieldReaderArray) {
+                Class fieldClass = fieldReader.fieldClass;
+                int var = mwc.var(fieldReader);
+                if (fieldClass == byte.class || fieldClass == short.class || fieldClass == int.class || fieldClass == boolean.class || fieldClass == char.class) {
+                    mw.iconst_0();
+                    mw.istore(var);
+                } else if (fieldClass == long.class) {
+                    mw.lconst_0();
+                    mw.lstore(var);
+                } else if (fieldClass == float.class) {
+                    mw.iconst_0();
+                    mw.i2f();
+                    mw.fstore(var);
+                } else if (fieldClass == double.class) {
+                    mw.iconst_0();
+                    mw.i2d();
+                    mw.dstore(var);
+                } else {
+                    mw.aconst_null();
+                    mw.astore(var);
+                }
+            }
+        } else {
+            genCreateObject(mw, context, classNameType);
+            mw.astore(OBJECT);
+        }
 
         mw.aload(JSON_READER);
         mw.invokevirtual(TYPE_JSON_READER, "nextIfObjectStart", "()Z");
         mw.pop();
 
         // for (int i = 0; i < entry_cnt; ++i) {
-        Label for_start_i_ = new Label(), for_end_i_ = new Label(), for_inc_i_ = new Label();
+        Label L_FOR_START = new Label(), L_FOR_END = new Label(), L_FOR_INC = new Label();
         if (!disableAutoType) {
             mw.iconst_0();
             mw.istore(I);
         }
 
-        mw.visitLabel(for_start_i_);
+        mw.visitLabel(L_FOR_START);
 
         Label hashCode64Start = new Label();
 
         mw.aload(JSON_READER);
         mw.invokevirtual(TYPE_JSON_READER, "nextIfObjectEnd", "()Z");
-        mw.ifne(for_end_i_);
+        mw.ifne(L_FOR_END);
 
+        boolean switchGen = false;
         if (context.fieldNameLengthMin >= 2 && context.fieldNameLengthMax <= 43) {
             genRead243(
                     context,
                     fieldBased,
                     mwc,
                     OBJECT,
-                    for_inc_i_,
+                    L_FOR_INC,
                     hashCode64Start
             );
+            switchGen = true;
         }
 
         mw.visitLabel(hashCode64Start);
@@ -1096,9 +1159,14 @@ public class ObjectReaderCreatorASM
         mw.lstore(HASH_CODE64);
         mw.lconst_0();
         mw.lcmp();
-        mw.ifeq(for_inc_i_);
+        mw.ifeq(L_FOR_INC);
 
-        if (!disableAutoType) {
+        if (!disableAutoType && !(context.objectReaderAdapter instanceof ObjectReaderNoneDefaultConstructor)) {
+            /*
+             * if (hashCode64 == this.typeKeyHashCode() && hashCode64 != 0) {
+             *      object = this.autoType(jsonReader);
+             * }
+             */
             Label endAutoType_ = new Label();
             mw.lload(HASH_CODE64);
             mw.aload(THIS);
@@ -1116,13 +1184,63 @@ public class ObjectReaderCreatorASM
             mw.aload(JSON_READER);
             mw.invokevirtual(classNameType, "autoType", "(" + DESC_JSON_READER + ")Ljava/lang/Object;");
             mw.astore(OBJECT);
-            mw.goto_(for_end_i_);
+            mw.goto_(L_FOR_END);
 
             mw.visitLabel(endAutoType_);
         }
 
-        // continue
-        if (fieldReaderArray.length > 6) {
+        if (switchGen) {
+            if (context.objectReaderAdapter instanceof ObjectReaderNoneDefaultConstructor) {
+                int ORIDINAL = mwc.var("ordinal");
+                mw.aload(THIS);
+                mw.lload(HASH_CODE64);
+                mw.invokevirtual(TYPE_OBJECT_READER_ADAPTER, "getFieldOrdinal", "(J)I");
+                mw.istore(ORIDINAL);
+
+                Label dflt = new Label();
+                Label[] labels = new Label[fieldReaderArray.length];
+                int[] switchKeys = new int[fieldReaderArray.length];
+                for (int i = 0; i < fieldReaderArray.length; i++) {
+                    labels[i] = new Label();
+                    switchKeys[i] = i;
+                }
+
+                mw.iload(ORIDINAL);
+                mw.visitLookupSwitchInsn(dflt, switchKeys, labels);
+
+                for (int i = 0; i < fieldReaderArray.length; i++) {
+                    mw.visitLabel(labels[i]);
+                    FieldReader fieldReader = fieldReaderArray[i];
+                    genReadFieldValue(
+                            context,
+                            fieldReader,
+                            fieldBased,
+                            mwc,
+                            OBJECT,
+                            i,
+                            false
+                    );
+                    mw.goto_(L_FOR_INC);
+                }
+
+                // jsonReader.skipValue();
+                mw.visitLabel(dflt);
+                mw.aload(JSON_READER);
+                mw.invokevirtual(TYPE_JSON_READER, "skipValue", "()V");
+            } else {
+                /*
+                 * this.readFieldValue(hashCode64, jsonReader, features, object);
+                 */
+                mw.aload(THIS);
+                mw.lload(HASH_CODE64);
+                mw.aload(JSON_READER);
+                mw.lload(FEATURES);
+                mw.aload(OBJECT);
+                mw.invokevirtual(TYPE_OBJECT_READER_ADAPTER, "readFieldValue", READ_FIELD_READER_UL);
+            }
+            mw.goto_(L_FOR_INC); // continue
+            // continue
+        } else if (fieldReaderArray.length > 6) {
             // use switch
             Map<Integer, List<Long>> map = new TreeMap();
 
@@ -1188,14 +1306,14 @@ public class ObjectReaderCreatorASM
                             index,
                             true // JSONB
                     );
-                    mw.goto_(for_inc_i_);
+                    mw.goto_(L_FOR_INC);
 
                     if (next != dflt) {
                         mw.visitLabel(next);
                     }
                 }
 
-                mw.goto_(for_inc_i_);
+                mw.goto_(L_FOR_INC);
             }
 
             // switch_default
@@ -1222,7 +1340,7 @@ public class ObjectReaderCreatorASM
             mw.aload(JSON_READER);
             mw.aload(OBJECT);
             mw.invokevirtual(TYPE_FIELD_READE, "readFieldValueJSONB", METHOD_DESC_READ_FIELD_VALUE);
-            mw.goto_(for_inc_i_); // continue
+            mw.goto_(L_FOR_INC); // continue
 
             mw.visitLabel(fieldReaderNull_);
         } else {
@@ -1248,7 +1366,7 @@ public class ObjectReaderCreatorASM
                         false // arrayMapping
                 );
 
-                mw.goto_(for_inc_i_); // continue
+                mw.goto_(L_FOR_INC); // continue
 
                 mw.visitLabel(next_);
             }
@@ -1288,29 +1406,89 @@ public class ObjectReaderCreatorASM
                         false // arrayMapping
                 );
 
-                mw.goto_(for_inc_i_); // continue
+                mw.goto_(L_FOR_INC); // continue
 
                 mw.visitLabel(next_);
             }
             mw.visitLabel(processExtra_);
         }
 
-        mw.aload(THIS);
-        mw.aload(JSON_READER);
-        mw.aload(OBJECT);
-        mw.lload(FEATURES);
-        mw.invokevirtual(TYPE_OBJECT_READER_ADAPTER, "processExtra", METHOD_DESC_PROCESS_EXTRA);
-        mw.goto_(for_inc_i_); // continue
+        if (context.objectReaderAdapter instanceof ObjectReaderNoneDefaultConstructor) {
+            mw.aload(JSON_READER);
+            mw.invokevirtual(TYPE_JSON_READER, "skipValue", "()V");
+        } else {
+            mw.aload(THIS);
+            mw.aload(JSON_READER);
+            mw.aload(OBJECT);
+            mw.lload(FEATURES);
+            mw.invokevirtual(TYPE_OBJECT_READER_ADAPTER, "processExtra", METHOD_DESC_PROCESS_EXTRA);
+        }
+        mw.goto_(L_FOR_INC); // continue
 
-        mw.visitLabel(for_inc_i_);
+        mw.visitLabel(L_FOR_INC);
         if (!disableAutoType) {
             mw.visitIincInsn(I, 1);
         }
-        mw.goto_(for_start_i_);
+        mw.goto_(L_FOR_START);
 
-        mw.visitLabel(for_end_i_);
+        mw.visitLabel(L_FOR_END);
 
-        mw.aload(OBJECT);
+        if (context.objectReaderAdapter instanceof ObjectReaderNoneDefaultConstructor) {
+            ObjectReaderNoneDefaultConstructor objectReaderNoneDefaultConstructor = (ObjectReaderNoneDefaultConstructor) context.objectReaderAdapter;
+            boolean constructDirect = true;
+            if (classLoader.isExternalClass(context.objectClass)
+                    || context.objectClass.getTypeParameters().length != 0
+                    || (objectReaderNoneDefaultConstructor.constructor != null && !Modifier.isPublic(objectReaderNoneDefaultConstructor.constructor.getModifiers()))
+                    || (context.objectClass != null && !Modifier.isPublic(context.objectClass.getModifiers()))
+                    || objectReaderNoneDefaultConstructor.factoryFunction != null
+                    || objectReaderNoneDefaultConstructor.noneDefaultConstructor != null && !Modifier.isPublic(objectReaderNoneDefaultConstructor.noneDefaultConstructor.getModifiers())
+            ) {
+                constructDirect = false;
+            }
+
+            if (constructDirect) {
+                mw.new_(context.objectType);
+                mw.dup();
+                StringBuilder buf = new StringBuilder().append("(");
+                for (FieldReader fieldReader : fieldReaderArray) {
+                    mw.loadLocal(fieldReader.fieldClass, mwc.var(fieldReader));
+                    buf.append(ASMUtils.desc(fieldReader.fieldClass));
+                }
+                buf.append(")V");
+                mw.invokespecial(context.objectType, "<init>", buf.toString());
+            } else {
+                mw.aload(THIS);
+                mw.iconst_n(fieldReaderArray.length);
+                mw.anewArray("java/lang/Object");
+                for (int i = 0; i < fieldReaderArray.length; i++) {
+                    FieldReader fieldReader = fieldReaderArray[i];
+                    mw.dup();
+                    mw.iconst_n(i);
+                    mw.loadLocal(fieldReader.fieldClass, mwc.var(fieldReader));
+                    if (fieldReader.fieldClass == int.class) {
+                        mw.invokestatic("java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;");
+                    } else if (fieldReader.fieldClass == long.class) {
+                        mw.invokestatic("java/lang/Long", "valueOf", "(J)Ljava/lang/Long;");
+                    } else if (fieldReader.fieldClass == float.class) {
+                        mw.invokestatic("java/lang/Float", "valueOf", "(F)Ljava/lang/Float;");
+                    } else if (fieldReader.fieldClass == double.class) {
+                        mw.invokestatic("java/lang/Double", "valueOf", "(D)Ljava/lang/Double;");
+                    } else if (fieldReader.fieldClass == boolean.class) {
+                        mw.invokestatic("java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;");
+                    } else if (fieldReader.fieldClass == short.class) {
+                        mw.invokestatic("java/lang/Short", "valueOf", "(S)Ljava/lang/Short;");
+                    } else if (fieldReader.fieldClass == byte.class) {
+                        mw.invokestatic("java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;");
+                    } else if (fieldReader.fieldClass == char.class) {
+                        mw.invokestatic("java/lang/Character", "valueOf", "(C)Ljava/lang/Character;");
+                    }
+                    mw.aastore();
+                }
+                mw.invokevirtual(TYPE_OBJECT_READER_NONE_DEFAULT_CONSTRUCTOR, "createInstance", "([Ljava/lang/Object;)Ljava/lang/Object;");
+            }
+        } else {
+            mw.aload(OBJECT);
+        }
         mw.areturn();
 
         mw.visitMaxs(5, 10);


Test patch:
diff --git a/core/src/test/java/com/alibaba/fastjson2/codec/NonDefaulConstructorTestTest2.java b/core/src/test/java/com/alibaba/fastjson2/codec/NonDefaulConstructorTestTest2.java
index ed456ac5b..46caf844e 100644
--- a/core/src/test/java/com/alibaba/fastjson2/codec/NonDefaulConstructorTestTest2.java
+++ b/core/src/test/java/com/alibaba/fastjson2/codec/NonDefaulConstructorTestTest2.java
@@ -1,15 +1,13 @@
 package com.alibaba.fastjson2.codec;
 
-import com.alibaba.fastjson2.JSON;
-import com.alibaba.fastjson2.JSONB;
-import com.alibaba.fastjson2.JSONObject;
-import com.alibaba.fastjson2.JSONWriter;
+import com.alibaba.fastjson2.*;
 import org.junit.jupiter.api.Test;
 
 import java.math.BigDecimal;
 import java.util.Currency;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 public class NonDefaulConstructorTestTest2 {
     @Test
@@ -97,4 +95,76 @@ public class NonDefaulConstructorTestTest2 {
             this.name = name;
         }
     }
+
+    @Test
+    public void test_bean1() {
+        JSONObject jsonObject = JSONObject.of("v0", 3);
+
+        {
+            String str = jsonObject.toString();
+            Bean1 a = JSON.parseObject(str, Bean1.class);
+            assertEquals(3, a.v0);
+        }
+
+        {
+            byte[] jsonb = JSONB.toBytes(jsonObject);
+            Bean1 a = JSONB.parseObject(jsonb, Bean1.class);
+            assertEquals(3, a.v0);
+        }
+    }
+
+    @Test
+    public void test_bean1_eror() {
+        byte[] jsonb = JSONObject.of("v0", null).toJSONBBytes(JSONWriter.Feature.WriteNulls);
+        assertThrows(
+                JSONException.class,
+                () -> JSONB.parseObject(jsonb, Bean1.class, JSONReader.Feature.ErrorOnNullForPrimitives));
+    }
+
+    public static class Bean1 {
+        final byte v0;
+        public Bean1(byte v0) {
+            this.v0 = v0;
+        }
+    }
+
+    @Test
+    public void test_ben6() {
+        JSONObject jsonObject = JSONObject.of("v0", 3);
+
+        String str = jsonObject.toString();
+        Bean6 a = JSON.parseObject(str, Bean6.class);
+        assertEquals(3, a.v0);
+
+        byte[] jsonb = JSONB.toBytes(jsonObject);
+        a = JSONB.parseObject(jsonb, Bean6.class);
+        assertEquals(3, a.v0);
+    }
+
+    @Test
+    public void test_ben6_error() {
+        byte[] jsonb = JSONObject.of("v0", null).toJSONBBytes(JSONWriter.Feature.WriteNulls);
+        assertThrows(
+                JSONException.class,
+                () -> JSONB.parseObject(jsonb, Bean6.class, JSONReader.Feature.ErrorOnNullForPrimitives));
+    }
+
+    public static class Bean6 {
+        final int v0;
+        final int v1;
+        final int v2;
+        final int v3;
+        final int v4;
+        final int v5;
+        final int v6;
+        public Bean6(int v0, int v1, int v2, int v3, int v4, int v5, int v6) {
+            this.v0 = v0;
+            this.v1 = v1;
+            this.v2 = v2;
+            this.v3 = v3;
+            this.v4 = v4;
+            this.v5 = v5;
+            this.v6 = v6;
+        }
+    }
 }
diff --git a/test-jdk17/src/test/java/com/alibaba/fastjson2/RecordTest.java b/test-jdk17/src/test/java/com/alibaba/fastjson2/RecordTest.java
index ed7af3326..d55b11b77 100644
--- a/test-jdk17/src/test/java/com/alibaba/fastjson2/RecordTest.java
+++ b/test-jdk17/src/test/java/com/alibaba/fastjson2/RecordTest.java
@@ -21,6 +21,10 @@ public class RecordTest {
 
         Item item2 = JSON.parseObject(str, Item.class);
         assertEquals(item.value, item2.value);
+
+        byte[] jsonb = JSONB.toBytes(item, JSONWriter.Feature.UseSingleQuotes);
+        Item item3 = JSONB.parseObject(jsonb, Item.class);
+        assertEquals(item.value, item2.value);
     }
 
     public record Item(int value) {
@@ -29,9 +33,21 @@ public class RecordTest {
     @Test
     public void test1() {
         Item1 item = new Item1(Arrays.asList("abc"));
-        String str = JSON.toJSONString(item);
-        Item1 item2 = JSON.parseObject(str, Item1.class);
-        assertEquals(item.value, item2.value);
+        {
+            String str = JSON.toJSONString(item);
+            Item1 item2 = JSON.parseObject(str, Item1.class);
+            assertEquals(item.value, item2.value);
+        }
+        {
+            String str = JSON.toJSONString(item, JSONWriter.Feature.UseSingleQuotes);
+            Item1 item2 = JSON.parseObject(str, Item1.class);
+            assertEquals(item.value, item2.value);
+        }
+        {
+            byte[] jsonb = JSONB.toBytes(item, JSONWriter.Feature.UseSingleQuotes);
+            Item1 item2 = JSONB.parseObject(jsonb, Item1.class);
+            assertEquals(item.value, item2.value);
+        }
     }
 
     public record Item1(List<String> value) {
@@ -44,6 +60,11 @@ public class RecordTest {
         Item2 item2 = JSON.parseObject(str, Item2.class);
         assertEquals(item.value.size(), item2.value.size());
         assertEquals(item.value.get(0).value, item2.value.get(0).value);
+
+        byte[] jsonb = JSONB.toBytes(item, JSONWriter.Feature.UseSingleQuotes);
+        Item2 item3 = JSONB.parseObject(jsonb, Item2.class);
+        assertEquals(item.value.size(), item3.value.size());
+        assertEquals(item.value.get(0).value, item3.value.get(0).value);
     }
 
     public record Item2(List<Item> value) {
@@ -88,6 +109,11 @@ public class RecordTest {
         Item5 item2 = JSON.parseObject(str, Item5.class);
         assertEquals(item.user.default_profile, item2.user.default_profile);
         assertEquals(item.user.screen_name, item2.user.screen_name);
+
+        byte[] jsonb = JSONB.toBytes(item);
+        Item5 item3 = JSONB.parseObject(jsonb, Item5.class);
+        assertEquals(item.user.default_profile, item3.user.default_profile);
+        assertEquals(item.user.screen_name, item3.user.screen_name);
     }
 
     private record User5(boolean default_profile, String screen_name) {