JSONB.java

package com.alibaba.fastjson2;

import com.alibaba.fastjson2.filter.Filter;
import com.alibaba.fastjson2.internal.trove.map.hash.TLongIntHashMap;
import com.alibaba.fastjson2.reader.ObjectReader;
import com.alibaba.fastjson2.reader.ObjectReaderBean;
import com.alibaba.fastjson2.reader.ObjectReaderProvider;
import com.alibaba.fastjson2.util.*;
import com.alibaba.fastjson2.writer.ObjectWriter;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.*;
import java.util.*;

import static com.alibaba.fastjson2.JSONB.Constants.*;
import static com.alibaba.fastjson2.JSONFactory.*;
import static com.alibaba.fastjson2.JSONWriter.*;
import static com.alibaba.fastjson2.JSONWriter.Feature.*;
import static com.alibaba.fastjson2.util.IOUtils.*;
import static com.alibaba.fastjson2.util.JDKUtils.*;

/**
 * This is the main entry point for using fastjson2 binary format (JSONB) API.
 * JSONB is a high-performance binary serialization format that is more compact and faster than JSON text format.
 *
 * <p>JSONB binary format specification:
 * <pre>
 * x90          # type_char int
 * x91          # binary len_int32 bytes
 * x92          # type [str] symbol_int32 jsonb
 * x93          # reference
 *
 * x94 - xa3    # array_0 - array_15
 * xa4          # array len_int32 item*
 *
 * xa5          # object_end
 * xa6          # object_start
 *
 * xa7          # local time b0 b1 b2
 * xa8          # local datetime b0 b1 b2 b3 b4 b5 b6
 * xa9          # local date b0 b1 b2 b3
 * xab          # timestamp millis b0 b1 b2 b3 b4 b5 b6 b7
 * xac          # timestamp seconds b0 b1 b2 b3
 * xad          # timestamp minutes b0 b1 b2 b3
 * xae          # timestamp b0 b1 b2 b3 b4 b5 b6 b7 nano_int32
 *
 * xaf          # null
 * xb0          # boolean false
 * xb1          # boolean true
 * xb2          # double 0
 * xb3          # double 1
 * xb4          # double_long
 * xb5          # double
 * xb6          # float_int
 * xb7          # float
 * xb8          # decimal_long
 * xb9          # decimal
 * xba          # bigint_long
 * xbb          # bigint
 * xbc          # short
 * xbd          # byte
 * xbe          # long
 * xbf          # long encoded as 32-bit int
 * xc0 - xc7    # three-octet compact long (-x40000 to x3ffff)
 * xc8 - xd7    # two-octet compact long (-x800 to x7ff, xd0 is 0)
 * xd8 - xef    # one-octet compact long (-x8 to xf, e0 is 0)
 *
 * xf0 - xff    # one-octet compact int
 * x00 - x2f    # one-octet compact int
 *
 * x30 - x3f    # two-octet compact int (-x800 to x7ff)
 * x40 - x47    # three-octet compact int (-x40000 to x3ffff)
 * x48          # 32-bit signed integer ('I')
 *
 * x49 - x78    # ascii string length 0-47
 * x79          # ascii-8 string variable-length
 * x7a          # utf-8 string variable-length
 * x7b          # utf-16 string variable-length
 * x7c          # utf-16LE string variable-length
 * x7d          # utf-16BE string variable-length
 * x7e          # gb18030 string variable-length
 * x7f          # symbol
 * </pre>
 *
 * <p>Example usage:
 * <pre>
 * // 1. Convert object to JSONB bytes
 * User user = new User(1L, "John", 30);
 * byte[] jsonbBytes = JSONB.toBytes(user);
 *
 * // 2. Parse JSONB bytes to object
 * User parsedUser = JSONB.parseObject(jsonbBytes, User.class);
 *
 * // 3. Convert primitive values to JSONB bytes
 * byte[] intBytes = JSONB.toBytes(123);
 * byte[] strBytes = JSONB.toBytes("Hello World");
 * byte[] boolBytes = JSONB.toBytes(true);
 *
 * // 4. Parse JSONB bytes with features
 * User user = JSONB.parseObject(jsonbBytes, User.class, JSONReader.Feature.FieldBased);
 *
 * // 5. Convert JSONB bytes to JSON string for debugging
 * String jsonString = JSONB.toJSONString(jsonbBytes);
 *
 * // 6. Parse JSONB bytes to JSONObject
 * JSONObject jsonObject = JSONB.parseObject(jsonbBytes);
 *
 * // 7. Parse JSONB bytes to JSONArray
 * JSONArray jsonArray = JSONB.parseArray(jsonbBytes);
 *
 * // 8. Parse JSONB bytes to List
 * List&lt;User&gt; userList = JSONB.parseArray(jsonbBytes, User.class);
 * </pre>
 *
 * @since 2.0.0
 */
public interface JSONB {
    /**
     * Dumps the JSONB bytes to standard output for debugging purposes
     *
     * @param jsonbBytes the JSONB bytes to dump
     */
    static void dump(byte[] jsonbBytes) {
        System.out.println(
                JSONB.toJSONString(jsonbBytes, true)
        );
    }

    /**
     * Dumps the JSONB bytes to standard output for debugging purposes with a symbol table
     *
     * @param jsonbBytes the JSONB bytes to dump
     * @param symbolTable the symbol table to use
     */
    static void dump(byte[] jsonbBytes, SymbolTable symbolTable) {
        JSONBDump dump = new JSONBDump(jsonbBytes, symbolTable, true);
        String str = dump.toString();
        System.out.println(str);
    }

    /**
     * Constants for JSONB binary format specification
     *
     * @since 2.0.0
     */
    interface Constants {
        /** Binary character type int */
        byte BC_CHAR = -112;                    // 0x90
        /** Binary data with length */
        byte BC_BINARY = -111;                  // 0x91
        /** Typed object with symbol */
        byte BC_TYPED_ANY = -110;               // 0x92
        /** Reference to previously serialized object */
        byte BC_REFERENCE = -109;               // 0x93

        /** Fixed array length */
        int ARRAY_FIX_LEN = 15;
        /** Fixed array with 0 elements */
        byte BC_ARRAY_FIX_0 = -108;             // 0x94
        /** Minimum fixed array marker */
        byte BC_ARRAY_FIX_MIN = BC_ARRAY_FIX_0;
        /** Maximum fixed array marker */
        byte BC_ARRAY_FIX_MAX = BC_ARRAY_FIX_MIN + ARRAY_FIX_LEN; // -105
        /** Variable length array */
        byte BC_ARRAY = -92;                    // 0xa4 len_int32 item*

        /** Object end marker */
        byte BC_OBJECT_END = -91;               // 0xa5
        /** Object start marker */
        byte BC_OBJECT = -90;                   // 0xa6

        /** Local time */
        byte BC_LOCAL_TIME = -89;               // 0xa7 b0 b1 b2 nano_int32
        /** Local datetime */
        byte BC_LOCAL_DATETIME = -88;           // 0xa8 b0 b1 b2 b3 b4 b5 b6 nano_int32
        /** Local date */
        byte BC_LOCAL_DATE = -87;               // 0xa9 b0 b1 b2 b3
        /** Timestamp with timezone */
        byte BC_TIMESTAMP_WITH_TIMEZONE = -86;  // 0xaa b0 b1 b2 b3 b4 b5 b6 b7 str_zone
        /** Timestamp in milliseconds */
        byte BC_TIMESTAMP_MILLIS = -85;         // 0xab b0 b1 b2 b3 b4 b5 b6 b7
        /** Timestamp in seconds */
        byte BC_TIMESTAMP_SECONDS = -84;        // 0xac b0 b1 b2 b3
        /** Timestamp in minutes */
        byte BC_TIMESTAMP_MINUTES = -83;        // 0xad b0 b1 b2 b3
        /** Timestamp */
        byte BC_TIMESTAMP = -82;                // 0xae millis_8 + nano_int32

        /** Null value */
        byte BC_NULL = -81;             // 0xaf
        /** Boolean false */
        byte BC_FALSE = -80;            // 0xb0
        /** Boolean true */
        byte BC_TRUE = -79;             // 0xb1
        /** Double 0 */
        byte BC_DOUBLE_NUM_0 = -78;     // 0xb2
        /** Double 1 */
        byte BC_DOUBLE_NUM_1 = -77;     // 0xb3
        /** Double as long */
        byte BC_DOUBLE_LONG = -76;      // 0xb4
        /** Double */
        byte BC_DOUBLE = -75;           // 0xb5
        /** Float as int */
        byte BC_FLOAT_INT = -74;        // 0xb6
        /** Float */
        byte BC_FLOAT = -73;            // 0xb7
        /** Decimal as long */
        byte BC_DECIMAL_LONG = -72;     // 0xb8
        /** Decimal */
        byte BC_DECIMAL = -71;          // 0xb9
        /** BigInteger as long */
        byte BC_BIGINT_LONG = -70;      // 0xba
        /** BigInteger */
        byte BC_BIGINT = -69;           // 0xbb
        /** Short */
        byte BC_INT16 = -68;            // 0xbc b0 b1
        /** Byte */
        byte BC_INT8 = -67;             // 0xbd b0
        /** Long */
        byte BC_INT64 = -66;            // 0xbe b0 b1 b2 b3 b4 b5 b6 b7
        /** Long as int */
        byte BC_INT64_INT = -65;        // 0xbf b0 b1 b2 b3

        /** Minimum 3-byte compact long */
        int INT64_SHORT_MIN = -0x40000; // -262144
        /** Maximum 3-byte compact long */
        int INT64_SHORT_MAX = 0x3ffff;  // 262143

        /** Minimum 2-byte compact long */
        int INT64_BYTE_MIN = -0x800;    // -2048
        /** Maximum 2-byte compact long */
        int INT64_BYTE_MAX = 0x7ff;     // 2047

        /** Minimum 3-byte compact long marker */
        byte BC_INT64_SHORT_MIN = -64;  // 0xc0
        /** Zero 3-byte compact long marker */
        byte BC_INT64_SHORT_ZERO = -60; //
        /** Maximum 3-byte compact long marker */
        byte BC_INT64_SHORT_MAX = -57;  // 0xc7

        /** Minimum 2-byte compact long marker */
        byte BC_INT64_BYTE_MIN = -56;   // 0xc8
        /** Zero 2-byte compact long marker */
        byte BC_INT64_BYTE_ZERO = -48;
        /** Maximum 2-byte compact long marker */
        byte BC_INT64_BYTE_MAX = -41;   // 0xd7

        /** Minimum 1-byte compact long marker */
        byte BC_INT64_NUM_MIN = -40;    // 0xd8 -8
        /** Maximum 1-byte compact long marker */
        byte BC_INT64_NUM_MAX = -17;    // 0xef 15

        /** Minimum 1-byte compact long value */
        int INT64_NUM_LOW_VALUE = -8;  // -8
        /** Maximum 1-byte compact long value */
        int INT64_NUM_HIGH_VALUE = 15; // 15

        /** Integer 0 */
        byte BC_INT32_NUM_0 = 0;
        /** Integer 1 */
        byte BC_INT32_NUM_1 = 1;
        /** Integer 16 */
        byte BC_INT32_NUM_16 = 16;

        /** Minimum 1-byte compact int */
        byte BC_INT32_NUM_MIN = -16; // 0xf0
        /** Maximum 1-byte compact int */
        byte BC_INT32_NUM_MAX = 47;  // 0x2f

        /** Minimum 2-byte compact int marker */
        byte BC_INT32_BYTE_MIN = 48;    // 0x30
        /** Zero 2-byte compact int marker */
        byte BC_INT32_BYTE_ZERO = 56;   // 0x38
        /** Maximum 2-byte compact int marker */
        byte BC_INT32_BYTE_MAX = 63;    // 0x3f

        /** Minimum 3-byte compact int marker */
        byte BC_INT32_SHORT_MIN = 64; // 0x40
        /** Zero 3-byte compact int marker */
        byte BC_INT32_SHORT_ZERO = 68;
        /** Maximum 3-byte compact int marker */
        byte BC_INT32_SHORT_MAX = 71; // 0x47
        /** 32-bit signed integer */
        byte BC_INT32 = 72; // 0x48

        /** Minimum 2-byte compact int value */
        int INT32_BYTE_MIN = -0x800; // -2048
        /** Maximum 2-byte compact int value */
        int INT32_BYTE_MAX = 0x7ff;  // 2047

        /** Minimum 3-byte compact int value */
        int INT32_SHORT_MIN = -0x40000; // -262144
        /** Maximum 3-byte compact int value */
        int INT32_SHORT_MAX = 0x3ffff;  // 262143

        /** ASCII string with 0 characters */
        byte BC_STR_ASCII_FIX_0 = 73;
        /** ASCII string with 1 character */
        byte BC_STR_ASCII_FIX_1 = 74;
        /** ASCII string with 4 characters */
        byte BC_STR_ASCII_FIX_4 = 77;
        /** ASCII string with 5 characters */
        byte BC_STR_ASCII_FIX_5 = 78;

        /** ASCII string with 32 characters */
        byte BC_STR_ASCII_FIX_32 = 105;
        /** ASCII string with 36 characters */
        byte BC_STR_ASCII_FIX_36 = 109;

        /** Fixed ASCII string length */
        int STR_ASCII_FIX_LEN = 47;

        /** Minimum fixed ASCII string marker */
        byte BC_STR_ASCII_FIX_MIN = 73; // 0x49
        /** Maximum fixed ASCII string marker */
        byte BC_STR_ASCII_FIX_MAX = BC_STR_ASCII_FIX_MIN + STR_ASCII_FIX_LEN; // 120 0x78
        /** Variable length ASCII string */
        byte BC_STR_ASCII = 121;
        /** UTF-8 string */
        byte BC_STR_UTF8 = 122;
        /** UTF-16 string */
        byte BC_STR_UTF16 = 123;
        /** UTF-16LE string */
        byte BC_STR_UTF16LE = 124;
        /** UTF-16BE string */
        byte BC_STR_UTF16BE = 125;
        /** GB18030 string */
        byte BC_STR_GB18030 = 126;
        /** Symbol */
        byte BC_SYMBOL = 127;
    }

    /**
     * Converts a boolean value to JSONB bytes
     *
     * @param v the boolean value to convert
     * @return the JSONB bytes representation
     */
    static byte[] toBytes(boolean v) {
        return new byte[]{v ? BC_TRUE : BC_FALSE};
    }

    /**
     * Converts an integer value to JSONB bytes
     *
     * @param i the integer value to convert
     * @return the JSONB bytes representation
     */
    static byte[] toBytes(int i) {
        if (i >= BC_INT32_NUM_MIN && i <= BC_INT32_NUM_MAX) {
            return new byte[]{(byte) i};
        }

        try (JSONWriter jsonWriter = JSONWriter.ofJSONB()) {
            jsonWriter.writeInt32(i);
            return jsonWriter.getBytes();
        }
    }

    /**
     * Converts a byte value to JSONB bytes
     *
     * @param i the byte value to convert
     * @return the JSONB bytes representation
     */
    static byte[] toBytes(byte i) {
        try (JSONWriter jsonWriter = JSONWriter.ofJSONB()) {
            jsonWriter.writeInt8(i);
            return jsonWriter.getBytes();
        }
    }

    /**
     * Converts a short value to JSONB bytes
     *
     * @param i the short value to convert
     * @return the JSONB bytes representation
     */
    static byte[] toBytes(short i) {
        try (JSONWriter jsonWriter = JSONWriter.ofJSONB()) {
            jsonWriter.writeInt16(i);
            return jsonWriter.getBytes();
        }
    }

    /**
     * Converts a long value to JSONB bytes
     *
     * @param i the long value to convert
     * @return the JSONB bytes representation
     */
    static byte[] toBytes(long i) {
        if (i >= INT64_NUM_LOW_VALUE && i <= INT64_NUM_HIGH_VALUE) {
            return new byte[]{
                    (byte) (BC_INT64_NUM_MIN + (i - INT64_NUM_LOW_VALUE))
            };
        }

        try (JSONWriter jsonWriter = JSONWriter.ofJSONB()) {
            jsonWriter.writeInt64(i);
            return jsonWriter.getBytes();
        }
    }

    /**
     * Parses JSONB bytes to an object using the specified context
     *
     * @param jsonbBytes the JSONB bytes to parse
     * @param context the JSON reader context
     * @return the parsed object
     * @since 2.0.46
     */
    static Object parse(byte[] jsonbBytes, JSONReader.Context context) {
        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            Object object = reader.readAnyObject();
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes to an object with specified features
     *
     * @param jsonbBytes the JSONB bytes to parse
     * @param features the JSON reader features to apply
     * @return the parsed object
     */
    static Object parse(byte[] jsonbBytes, JSONReader.Feature... features) {
        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                new JSONReader.Context(getDefaultObjectReaderProvider(), features),
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            Object object = reader.readAnyObject();
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB from an input stream to an object using the specified context
     *
     * @param in the input stream to parse from
     * @param context the JSON reader context
     * @return the parsed object
     */
    static Object parse(InputStream in, JSONReader.Context context) {
        try (JSONReaderJSONB reader = new JSONReaderJSONB(context, in)) {
            Object object = reader.readAny();
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes to an object with a symbol table and features
     *
     * @param jsonbBytes the JSONB bytes to parse
     * @param symbolTable the symbol table to use
     * @param features the JSON reader features to apply
     * @return the parsed object
     */
    static Object parse(byte[] jsonbBytes, SymbolTable symbolTable, JSONReader.Feature... features) {
        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                new JSONReader.Context(getDefaultObjectReaderProvider(), symbolTable, features),
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            Object object = reader.readAnyObject();
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes to a JSONObject
     *
     * @param jsonbBytes the JSONB bytes to parse
     * @return the parsed JSONObject
     */
    static JSONObject parseObject(byte[] jsonbBytes) {
        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                new JSONReader.Context(JSONFactory.getDefaultObjectReaderProvider()),
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            JSONObject object = (JSONObject) reader.readObject();
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes to a JSONObject with specified features
     *
     * @param jsonbBytes the JSONB bytes to parse
     * @param features the JSON reader features to apply
     * @return the parsed JSONObject
     */
    static JSONObject parseObject(byte[] jsonbBytes, JSONReader.Feature... features) {
        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                new JSONReader.Context(JSONFactory.getDefaultObjectReaderProvider(), features),
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            JSONObject object = (JSONObject) reader.readObject();
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes to a JSONObject using the specified context
     *
     * @param in the input stream to parse from
     * @param context the JSON reader context
     * @return the parsed JSONObject
     */
    static JSONObject parseObject(InputStream in, JSONReader.Context context) {
        try (JSONReaderJSONB reader = new JSONReaderJSONB(context, in)) {
            JSONObject object = (JSONObject) reader.readObject();
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes to a JSONArray
     *
     * @param jsonbBytes the JSONB bytes to parse
     * @return the parsed JSONArray
     */
    static JSONArray parseArray(byte[] jsonbBytes) {
        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                new JSONReader.Context(JSONFactory.getDefaultObjectReaderProvider()),
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            JSONArray array = (JSONArray) reader.readArray();
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(array);
            }
            return array;
        }
    }

    /**
     * Parses JSONB from an input stream to a JSONArray using the specified context
     *
     * @param in the input stream to parse from
     * @param context the JSON reader context
     * @return the parsed JSONArray
     */
    static JSONArray parseArray(InputStream in, JSONReader.Context context) {
        try (JSONReaderJSONB reader = new JSONReaderJSONB(context, in)) {
            JSONArray array = (JSONArray) reader.readArray();
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(array);
            }
            return array;
        }
    }

    /**
     * Parses JSONB bytes to a list of objects of the specified type
     *
     * @param <T> the type of objects in the list
     * @param jsonbBytes the JSONB bytes to parse
     * @param type the type of objects in the list
     * @return the parsed list of objects
     */
    static <T> List<T> parseArray(byte[] jsonbBytes, Type type) {
        if (jsonbBytes == null || jsonbBytes.length == 0) {
            return null;
        }

        Type paramType = new ParameterizedTypeImpl(
                new Type[]{type}, null, List.class
        );

        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                new JSONReader.Context(JSONFactory.getDefaultObjectReaderProvider()),
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            List<T> list = reader.read(paramType);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(list);
            }
            return list;
        }
    }

    /**
     * Parses JSONB bytes to a list of objects of the specified type with features
     *
     * @param <T> the type of objects in the list
     * @param jsonbBytes the JSONB bytes to parse
     * @param type the type of objects in the list
     * @param features the JSON reader features to apply
     * @return the parsed list of objects
     */
    static <T> List<T> parseArray(byte[] jsonbBytes, Type type, JSONReader.Feature... features) {
        if (jsonbBytes == null || jsonbBytes.length == 0) {
            return null;
        }

        Type paramType = new ParameterizedTypeImpl(
                new Type[]{type}, null, List.class
        );

        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                new JSONReader.Context(JSONFactory.getDefaultObjectReaderProvider(), features),
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            List<T> list = reader.read(paramType);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(list);
            }
            return list;
        }
    }

    /**
     * Parses JSONB bytes to a list of objects with specified types
     *
     * @param <T> the type of objects in the list
     * @param jsonbBytes the JSONB bytes to parse
     * @param types the types of objects in the list
     * @return the parsed list of objects
     */
    static <T> List<T> parseArray(byte[] jsonbBytes, Type... types) {
        if (jsonbBytes == null || jsonbBytes.length == 0) {
            return null;
        }

        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                new JSONReader.Context(JSONFactory.getDefaultObjectReaderProvider()),
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            List<T> list = reader.readList(types);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(list);
            }
            return list;
        }
    }

    /**
     * Parses JSONB bytes to a list of objects with specified types and features
     *
     * @param <T> the type of objects in the list
     * @param jsonbBytes the JSONB bytes to parse
     * @param types the types of objects in the list
     * @param features the JSON reader features to apply
     * @return the parsed list of objects
     */
    static <T> List<T> parseArray(byte[] jsonbBytes, Type[] types, JSONReader.Feature... features) {
        if (jsonbBytes == null || jsonbBytes.length == 0) {
            return null;
        }

        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                new JSONReader.Context(JSONFactory.getDefaultObjectReaderProvider(), features),
                jsonbBytes,
                0, jsonbBytes.length)
        ) {
            List<T> list = reader.readList(types);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(list);
            }
            return list;
        }
    }

    /**
     * Parses JSONB bytes to an object of the specified class
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param objectClass the class of the object to parse to
     * @return the parsed object
     */
    static <T> T parseObject(byte[] jsonbBytes, Class<T> objectClass) {
        ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();
        try (JSONReaderJSONB jsonReader = new JSONReaderJSONB(
                new JSONReader.Context(provider),
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            Object object;
            if (objectClass == Object.class) {
                object = jsonReader.readAny();
            } else {
                ObjectReader objectReader = provider.getObjectReader(
                        objectClass,
                        (defaultReaderFeatures & JSONReader.Feature.FieldBased.mask) != 0
                );
                object = objectReader.readJSONBObject(jsonReader, objectClass, null, 0);
            }

            if (jsonReader.resolveTasks != null) {
                jsonReader.handleResolveTasks(object);
            }
            return (T) object;
        }
    }

    /**
     * Parses JSONB bytes to an object of the specified type
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param objectType the type of the object to parse to
     * @return the parsed object
     */
    static <T> T parseObject(byte[] jsonbBytes, Type objectType) {
        ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();
        ObjectReader objectReader = provider.getObjectReader(objectType);

        try (JSONReaderJSONB jsonReader = new JSONReaderJSONB(
                new JSONReader.Context(provider),
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            T object = (T) objectReader.readJSONBObject(jsonReader, objectType, null, 0);
            if (jsonReader.resolveTasks != null) {
                jsonReader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes to an object with specified types
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param types the types of the object to parse to
     * @return the parsed object
     */
    static <T> T parseObject(byte[] jsonbBytes, Type... types) {
        return parseObject(jsonbBytes, new MultiType(types));
    }

    /**
     * Parses JSONB bytes to an object of the specified type with a symbol table
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param objectType the type of the object to parse to
     * @param symbolTable the symbol table to use
     * @return the parsed object
     */
    static <T> T parseObject(byte[] jsonbBytes, Type objectType, SymbolTable symbolTable) {
        ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();
        ObjectReader objectReader = provider.getObjectReader(objectType);

        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                new JSONReader.Context(provider, symbolTable),
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            T object = (T) objectReader.readJSONBObject(reader, objectType, null, 0);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes to an object of the specified type with a symbol table and features
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param objectType the type of the object to parse to
     * @param symbolTable the symbol table to use
     * @param features the JSON reader features to apply
     * @return the parsed object
     */
    static <T> T parseObject(
            byte[] jsonbBytes,
            Type objectType,
            SymbolTable symbolTable,
            JSONReader.Feature... features
    ) {
        ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();
        JSONReader.Context context = new JSONReader.Context(provider, symbolTable, features);
        boolean fieldBased = (context.features & JSONReader.Feature.FieldBased.mask) != 0;
        ObjectReader objectReader = provider.getObjectReader(objectType, fieldBased);

        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            T object = (T) objectReader.readJSONBObject(reader, objectType, null, 0);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes to an object of the specified class with a filter and features
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param objectClass the class of the object to parse to
     * @param filter the filter to apply
     * @param features the JSON reader features to apply
     * @return the parsed object
     */
    static <T> T parseObject(
            byte[] jsonbBytes,
            Class<T> objectClass,
            Filter filter,
            JSONReader.Feature... features
    ) {
        ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();
        JSONReader.Context context = new JSONReader.Context(provider, filter, features);

        try (JSONReaderJSONB jsonReader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            for (int i = 0; i < features.length; i++) {
                context.features |= features[i].mask;
            }

            Object object;
            if (objectClass == Object.class) {
                object = jsonReader.readAnyObject();
            } else {
                boolean fieldBased = (context.features & JSONReader.Feature.FieldBased.mask) != 0;
                ObjectReader objectReader = provider.getObjectReader(objectClass, fieldBased);
                object = objectReader.readJSONBObject(jsonReader, objectClass, null, 0);
            }

            if (jsonReader.resolveTasks != null) {
                jsonReader.handleResolveTasks(object);
            }
            return (T) object;
        }
    }

    /**
     * Parses JSONB bytes to an object of the specified type with a symbol table, filters and features
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param objectType the type of the object to parse to
     * @param symbolTable the symbol table to use
     * @param filters the filters to apply
     * @param features the JSON reader features to apply
     * @return the parsed object
     */
    static <T> T parseObject(
            byte[] jsonbBytes,
            Type objectType,
            SymbolTable symbolTable,
            Filter[] filters,
            JSONReader.Feature... features) {
        if (jsonbBytes == null || jsonbBytes.length == 0) {
            return null;
        }

        ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();
        JSONReader.Context context = new JSONReader.Context(provider, symbolTable, filters, features);

        try (JSONReaderJSONB jsonReader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            for (int i = 0; i < features.length; i++) {
                context.features |= features[i].mask;
            }

            Object object;
            if (objectType == Object.class) {
                object = jsonReader.readAnyObject();
            } else {
                boolean fieldBased = (context.features & JSONReader.Feature.FieldBased.mask) != 0;
                ObjectReader objectReader = provider.getObjectReader(objectType, fieldBased);
                object = objectReader.readJSONBObject(jsonReader, objectType, null, 0);
            }

            if (jsonReader.resolveTasks != null) {
                jsonReader.handleResolveTasks(object);
            }
            return (T) object;
        }
    }

    /**
     * Creates a deep copy of the specified object
     *
     * @param <T> the type of the object
     * @param object the object to copy
     * @param features the JSON writer features to apply
     * @return a deep copy of the object
     * @since 2.0.30
     */
    static <T> T copy(T object, JSONWriter.Feature... features) {
        return JSON.copy(object, features);
    }

    /**
     * Parses JSONB bytes to an object of the specified type reference
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param typeReference the type reference of the object to parse to
     * @param features the JSON reader features to apply
     * @return the parsed object
     */
    static <T> T parseObject(byte[] jsonbBytes, TypeReference typeReference, JSONReader.Feature... features) {
        return parseObject(jsonbBytes, typeReference.getType(), features);
    }

    /**
     * Parses JSONB from an input stream to an object of the specified class
     *
     * @param <T> the type of the object
     * @param in the input stream to parse from
     * @param objectClass the class of the object to parse to
     * @param features the JSON reader features to apply
     * @return the parsed object
     * @throws IOException if an I/O error occurs
     * @since 2.0.30
     */
    static <T> T parseObject(
            InputStream in,
            Class objectClass,
            JSONReader.Feature... features
    ) throws IOException {
        return parseObject(in, objectClass, JSONFactory.createReadContext(features));
    }

    /**
     * Parses JSONB from an input stream to an object of the specified type
     *
     * @param <T> the type of the object
     * @param in the input stream to parse from
     * @param objectType the type of the object to parse to
     * @param features the JSON reader features to apply
     * @return the parsed object
     * @throws IOException if an I/O error occurs
     * @since 2.0.30
     */
    static <T> T parseObject(
            InputStream in,
            Type objectType,
            JSONReader.Feature... features
    ) throws IOException {
        return parseObject(in, objectType, JSONFactory.createReadContext(features));
    }

    /**
     * Parses JSONB from an input stream to an object of the specified type using the specified context
     *
     * @param <T> the type of the object
     * @param in the input stream to parse from
     * @param objectType the type of the object to parse to
     * @param context the JSON reader context
     * @return the parsed object
     * @since 2.0.30
     */
    static <T> T parseObject(
            InputStream in,
            Type objectType,
            JSONReader.Context context
    ) {
        try (JSONReaderJSONB jsonReader = new JSONReaderJSONB(context, in)
        ) {
            Object object;
            if (objectType == Object.class) {
                object = jsonReader.readAny();
            } else {
                ObjectReader objectReader = context.getObjectReader(objectType);
                object = objectReader.readJSONBObject(jsonReader, objectType, null, 0);
            }

            if (jsonReader.resolveTasks != null) {
                jsonReader.handleResolveTasks(object);
            }
            return (T) object;
        }
    }

    /**
     * Parses JSONB from an input stream to an object of the specified class using the specified context
     *
     * @param <T> the type of the object
     * @param in the input stream to parse from
     * @param objectClass the class of the object to parse to
     * @param context the JSON reader context
     * @return the parsed object
     * @since 2.0.30
     */
    static <T> T parseObject(
            InputStream in,
            Class objectClass,
            JSONReader.Context context
    ) {
        try (JSONReaderJSONB jsonReader = new JSONReaderJSONB(context, in)) {
            Object object;
            if (objectClass == Object.class) {
                object = jsonReader.readAny();
            } else {
                ObjectReader objectReader = context.getObjectReader(objectClass);
                object = objectReader.readJSONBObject(jsonReader, objectClass, null, 0);
            }

            if (jsonReader.resolveTasks != null) {
                jsonReader.handleResolveTasks(object);
            }
            return (T) object;
        }
    }

    /**
     * Parses JSONB from an input stream with specified length to an object of the specified type using the specified context
     *
     * @param <T> the type of the object
     * @param in the input stream to parse from
     * @param length the length of data to read
     * @param objectType the type of the object to parse to
     * @param context the JSON reader context
     * @return the parsed object
     * @throws IOException if an I/O error occurs
     */
    static <T> T parseObject(
            InputStream in,
            int length,
            Type objectType,
            JSONReader.Context context
    ) throws IOException {
        int cacheIndex = System.identityHashCode(Thread.currentThread()) & (CACHE_ITEMS.length - 1);
        final CacheItem cacheItem = CACHE_ITEMS[cacheIndex];
        byte[] bytes = BYTES_UPDATER.getAndSet(cacheItem, null);
        if (bytes == null) {
            bytes = new byte[8192];
        }

        try {
            if (bytes.length < length) {
                bytes = new byte[length];
            }
            int read = in.read(bytes, 0, length);
            if (read != length) {
                throw new IllegalArgumentException("deserialize failed. expected read length: " + length + " but actual read: " + read);
            }

            return parseObject(bytes, 0, length, objectType, context);
        } finally {
            BYTES_UPDATER.lazySet(cacheItem, bytes);
        }
    }

    /**
     * Parses JSONB from an input stream with specified length to an object of the specified type with features
     *
     * @param <T> the type of the object
     * @param in the input stream to parse from
     * @param length the length of data to read
     * @param objectType the type of the object to parse to
     * @param features the JSON reader features to apply
     * @return the parsed object
     * @throws IOException if an I/O error occurs
     */
    static <T> T parseObject(
            InputStream in,
            int length,
            Type objectType,
            JSONReader.Feature... features
    ) throws IOException {
        int cacheIndex = System.identityHashCode(Thread.currentThread()) & (CACHE_ITEMS.length - 1);
        final CacheItem cacheItem = CACHE_ITEMS[cacheIndex];
        byte[] bytes = BYTES_UPDATER.getAndSet(cacheItem, null);
        if (bytes == null) {
            bytes = new byte[8192];
        }
        try {
            if (bytes.length < length) {
                bytes = new byte[length];
            }
            int read = in.read(bytes, 0, length);
            if (read != length) {
                throw new IllegalArgumentException("deserialize failed. expected read length: " + length + " but actual read: " + read);
            }

            return parseObject(bytes, 0, length, objectType, features);
        } finally {
            BYTES_UPDATER.lazySet(cacheItem, bytes);
        }
    }

    /**
     * Parses JSONB bytes to an object of the specified class with features
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param objectClass the class of the object to parse to
     * @param features the JSON reader features to apply
     * @return the parsed object
     */
    static <T> T parseObject(byte[] jsonbBytes, Class<T> objectClass, JSONReader.Feature... features) {
        ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();
        JSONReader.Context context = new JSONReader.Context(provider, features);

        try (JSONReaderJSONB jsonReader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            Object object;
            if (objectClass == Object.class) {
                object = jsonReader.readAnyObject();
            } else {
                boolean fieldBased = (context.features & JSONReader.Feature.FieldBased.mask) != 0;
                ObjectReader objectReader = provider.getObjectReader(objectClass, fieldBased);
                if ((context.features & JSONReader.Feature.SupportArrayToBean.mask) != 0
                        && jsonReader.isArray()
                        && objectReader instanceof ObjectReaderBean
                ) {
                    object = objectReader.readArrayMappingJSONBObject(jsonReader, objectClass, null, 0);
                } else {
                    object = objectReader.readJSONBObject(jsonReader, objectClass, null, 0);
                }
            }

            if (jsonReader.resolveTasks != null) {
                jsonReader.handleResolveTasks(object);
            }
            return (T) object;
        }
    }

    /**
     * Parses JSONB bytes to an object of the specified class using the specified context
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param objectClass the class of the object to parse to
     * @param context the JSON reader context
     * @return the parsed object
     */
    static <T> T parseObject(byte[] jsonbBytes, Class<T> objectClass, JSONReader.Context context) {
        try (JSONReaderJSONB jsonReader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                0,
                jsonbBytes.length)
        ) {
            Object object;
            if (objectClass == Object.class) {
                object = jsonReader.readAnyObject();
            } else {
                ObjectReader objectReader = context.provider.getObjectReader(
                        objectClass,
                        (context.features & JSONReader.Feature.FieldBased.mask) != 0
                );
                if ((context.features & JSONReader.Feature.SupportArrayToBean.mask) != 0
                        && jsonReader.isArray()
                        && objectReader instanceof ObjectReaderBean
                ) {
                    object = objectReader.readArrayMappingJSONBObject(jsonReader, objectClass, null, 0);
                } else {
                    object = objectReader.readJSONBObject(jsonReader, objectClass, null, 0);
                }
            }

            if (jsonReader.resolveTasks != null) {
                jsonReader.handleResolveTasks(object);
            }
            return (T) object;
        }
    }

    /**
     * Parses JSONB bytes to an object of the specified type with features
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param objectClass the type of the object to parse to
     * @param features the JSON reader features to apply
     * @return the parsed object
     */
    static <T> T parseObject(byte[] jsonbBytes, Type objectClass, JSONReader.Feature... features) {
        ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();
        JSONReader.Context context = new JSONReader.Context(provider, features);

        ObjectReader objectReader = provider.getObjectReader(
                objectClass,
                (context.features & JSONReader.Feature.FieldBased.mask) != 0
        );

        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                0,
                jsonbBytes.length)) {
            T object = (T) objectReader.readJSONBObject(reader, objectClass, null, 0);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes with offset and length to an object of the specified class
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param off the offset in the byte array
     * @param len the length of data to parse
     * @param objectClass the class of the object to parse to
     * @return the parsed object
     */
    static <T> T parseObject(byte[] jsonbBytes, int off, int len, Class<T> objectClass) {
        ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();
        JSONReader.Context context = new JSONReader.Context(provider);
        boolean fieldBased = (context.features & JSONReader.Feature.FieldBased.mask) != 0;
        ObjectReader objectReader = provider.getObjectReader(objectClass, fieldBased);

        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                off,
                len)
        ) {
            T object = (T) objectReader.readJSONBObject(reader, objectClass, null, 0);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes with offset and length to an object of the specified type
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param off the offset in the byte array
     * @param len the length of data to parse
     * @param type the type of the object to parse to
     * @return the parsed object
     */
    static <T> T parseObject(byte[] jsonbBytes, int off, int len, Type type) {
        ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();
        JSONReader.Context context = new JSONReader.Context(provider);
        boolean fieldBased = (context.features & JSONReader.Feature.FieldBased.mask) != 0;
        ObjectReader objectReader = provider.getObjectReader(type, fieldBased);

        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                off,
                len)
        ) {
            T object = (T) objectReader.readJSONBObject(reader, type, null, 0);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes with offset and length to an object of the specified class with features
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param off the offset in the byte array
     * @param len the length of data to parse
     * @param objectClass the class of the object to parse to
     * @param features the JSON reader features to apply
     * @return the parsed object
     */
    static <T> T parseObject(
            byte[] jsonbBytes,
            int off,
            int len,
            Class<T> objectClass,
            JSONReader.Feature... features
    ) {
        ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();
        JSONReader.Context context = new JSONReader.Context(provider, features);
        boolean fieldBased = (context.features & JSONReader.Feature.FieldBased.mask) != 0;
        ObjectReader objectReader = provider.getObjectReader(objectClass, fieldBased);

        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                off,
                len)
        ) {
            T object = (T) objectReader.readJSONBObject(reader, objectClass, null, 0);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes with offset and length to an object of the specified type using the specified context
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param off the offset in the byte array
     * @param len the length of data to parse
     * @param objectType the type of the object to parse to
     * @param context the JSON reader context
     * @return the parsed object
     */
    static <T> T parseObject(
            byte[] jsonbBytes,
            int off,
            int len,
            Type objectType,
            JSONReader.Context context
    ) {
        try (JSONReaderJSONB reader = new JSONReaderJSONB(context, jsonbBytes, off, len)) {
            boolean fieldBased = (context.features & JSONReader.Feature.FieldBased.mask) != 0;
            ObjectReader objectReader = context.provider.getObjectReader(objectType, fieldBased);

            T object = (T) objectReader.readJSONBObject(reader, objectType, null, 0);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes with offset and length to an object of the specified type with features
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param off the offset in the byte array
     * @param len the length of data to parse
     * @param objectType the type of the object to parse to
     * @param features the JSON reader features to apply
     * @return the parsed object
     */
    static <T> T parseObject(byte[] jsonbBytes, int off, int len, Type objectType, JSONReader.Feature... features) {
        JSONReader.Context context = createReadContext(features);
        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                off,
                len)
        ) {
            ObjectReader objectReader = reader.getObjectReader(objectType);

            T object = (T) objectReader.readJSONBObject(reader, objectType, null, 0);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes with offset and length to an object of the specified class with a symbol table
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param off the offset in the byte array
     * @param len the length of data to parse
     * @param objectClass the class of the object to parse to
     * @param symbolTable the symbol table to use
     * @return the parsed object
     */
    static <T> T parseObject(byte[] jsonbBytes, int off, int len, Class<T> objectClass, SymbolTable symbolTable) {
        JSONReader.Context context = createReadContext(symbolTable);
        ObjectReader objectReader = context.getObjectReader(objectClass);
        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                off,
                len)
        ) {
            T object = (T) objectReader.readJSONBObject(reader, objectClass, null, 0);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes with offset and length to an object of the specified type with a symbol table
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param off the offset in the byte array
     * @param len the length of data to parse
     * @param objectClass the type of the object to parse to
     * @param symbolTable the symbol table to use
     * @return the parsed object
     */
    static <T> T parseObject(byte[] jsonbBytes, int off, int len, Type objectClass, SymbolTable symbolTable) {
        JSONReader.Context context = createReadContext(symbolTable);
        ObjectReader objectReader = context.getObjectReader(objectClass);

        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                off,
                len)
        ) {
            T object = (T) objectReader.readJSONBObject(reader, objectClass, null, 0);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes with offset and length to an object of the specified class with a symbol table and features
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param off the offset in the byte array
     * @param len the length of data to parse
     * @param objectClass the class of the object to parse to
     * @param symbolTable the symbol table to use
     * @param features the JSON reader features to apply
     * @return the parsed object
     */
    static <T> T parseObject(
            byte[] jsonbBytes,
            int off,
            int len,
            Class<T> objectClass,
            SymbolTable symbolTable,
            JSONReader.Feature... features
    ) {
        JSONReader.Context context = createReadContext(symbolTable, features);
        ObjectReader objectReader = context.getObjectReader(objectClass);

        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                off,
                len)
        ) {
            T object = (T) objectReader.readJSONBObject(reader, objectClass, null, 0);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Parses JSONB bytes with offset and length to an object of the specified type with a symbol table and features
     *
     * @param <T> the type of the object
     * @param jsonbBytes the JSONB bytes to parse
     * @param off the offset in the byte array
     * @param len the length of data to parse
     * @param objectClass the type of the object to parse to
     * @param symbolTable the symbol table to use
     * @param features the JSON reader features to apply
     * @return the parsed object
     */
    static <T> T parseObject(
            byte[] jsonbBytes,
            int off,
            int len,
            Type objectClass,
            SymbolTable symbolTable,
            JSONReader.Feature... features
    ) {
        JSONReader.Context context = createReadContext(symbolTable, features);
        ObjectReader objectReader = context.getObjectReader(objectClass);

        try (JSONReaderJSONB reader = new JSONReaderJSONB(
                context,
                jsonbBytes,
                off,
                len)
        ) {
            T object = (T) objectReader.readJSONBObject(reader, objectClass, null, 0);
            if (reader.resolveTasks != null) {
                reader.handleResolveTasks(object);
            }
            return object;
        }
    }

    /**
     * Converts a string to JSONB bytes
     *
     * @param str the string to convert
     * @return the JSONB bytes representation
     */
    static byte[] toBytes(String str) {
        if (str == null) {
            return new byte[]{BC_NULL};
        }

        if (JVM_VERSION == 8) {
            char[] chars = JDKUtils.getCharArray(str);
            int strlen = chars.length;
            if (strlen <= STR_ASCII_FIX_LEN) {
                boolean ascii = true;
                for (int i = 0; i < strlen; ++i) {
                    if (chars[i] > 0x007F) {
                        ascii = false;
                        break;
                    }
                }

                if (ascii) {
                    byte[] bytes = new byte[chars.length + 1];
                    bytes[0] = (byte) (strlen + BC_STR_ASCII_FIX_MIN);
                    for (int i = 0; i < strlen; ++i) {
                        bytes[i + 1] = (byte) chars[i];
                    }
                    return bytes;
                }
            }
        } else if (STRING_VALUE != null) {
            int coder = STRING_CODER.applyAsInt(str);
            if (coder == 0) {
                byte[] value = STRING_VALUE.apply(str);
                int strlen = value.length;
                if (strlen <= STR_ASCII_FIX_LEN) {
                    byte[] bytes = new byte[value.length + 1];
                    bytes[0] = (byte) (strlen + BC_STR_ASCII_FIX_MIN);
                    System.arraycopy(value, 0, bytes, 1, value.length);
                    return bytes;
                }
            }
        }

        try (JSONWriterJSONB writer = new JSONWriterJSONB(
                new JSONWriter.Context(JSONFactory.defaultObjectWriterProvider),
                null
        )) {
            writer.writeString(str);
            return writer.getBytes();
        }
    }

    /**
     * Converts a string to JSONB bytes with specified charset
     *
     * @param str the string to convert
     * @param charset the charset to use
     * @return the JSONB bytes representation
     */
    static byte[] toBytes(String str, Charset charset) {
        if (str == null) {
            return new byte[]{BC_NULL};
        }

        final byte type;
        if (charset == StandardCharsets.UTF_16) {
            type = BC_STR_UTF16;
        } else if (charset == StandardCharsets.UTF_16BE) {
            type = BC_STR_UTF16BE;
        } else if (charset == StandardCharsets.UTF_16LE) {
            type = BC_STR_UTF16LE;
        } else if (charset == StandardCharsets.UTF_8) {
            type = BC_STR_UTF8;
        } else if (charset == StandardCharsets.US_ASCII || charset == StandardCharsets.ISO_8859_1) {
            type = BC_STR_ASCII;
        } else if (charset != null && "GB18030".equals(charset.name())) { // GraalVM support
            type = BC_STR_GB18030;
        } else {
            return toBytes(str);
        }

        byte[] utf16 = str.getBytes(charset);
        int byteslen = 2 + utf16.length;
        if (utf16.length <= BC_INT32_NUM_MAX) {
            byteslen += 0;
        } else if (utf16.length <= INT32_BYTE_MAX) {
            byteslen += 1;
        } else if (utf16.length <= INT32_SHORT_MAX) {
            byteslen += 2;
        } else {
            byteslen += 4;
        }

        byte[] bytes = new byte[byteslen];
        bytes[0] = type;
        int off = IO.writeInt32(bytes, 1, utf16.length);
        System.arraycopy(utf16, 0, bytes, off, utf16.length);
        return bytes;
    }

    /**
     * Converts an object to JSONB bytes
     *
     * @param object the object to convert
     * @return the JSONB bytes representation
     */
    static byte[] toBytes(Object object) {
        final JSONWriter.Context context = new JSONWriter.Context(defaultObjectWriterProvider);
        try (JSONWriterJSONB writer = new JSONWriterJSONB(
                context,
                null
        )) {
            if (object == null) {
                writer.writeNull();
            } else {
                Class<?> valueClass = object.getClass();
                boolean fieldBased = (context.features & JSONWriter.Feature.FieldBased.mask) != 0;
                ObjectWriter objectWriter = context.provider.getObjectWriter(valueClass, valueClass, fieldBased);
                objectWriter.writeJSONB(writer, object, null, null, 0);
            }
            return writer.getBytes();
        }
    }

    /**
     * Converts an object to JSONB bytes with specified context
     *
     * @param object the object to convert
     * @param context the JSON writer context
     * @return the JSONB bytes representation
     */
    static byte[] toBytes(Object object, JSONWriter.Context context) {
        if (context == null) {
            context = JSONFactory.createWriteContext();
        }

        try (JSONWriterJSONB writer = new JSONWriterJSONB(context, null)) {
            if (object == null) {
                writer.writeNull();
            } else {
                writer.rootObject = object;
                writer.path = JSONWriter.Path.ROOT;

                boolean fieldBased = (context.features & JSONWriter.Feature.FieldBased.mask) != 0;

                Class<?> valueClass = object.getClass();
                ObjectWriter objectWriter = context.provider.getObjectWriter(valueClass, valueClass, fieldBased);
                if ((context.features & JSONWriter.Feature.BeanToArray.mask) != 0) {
                    objectWriter.writeArrayMappingJSONB(writer, object, null, null, 0);
                } else {
                    objectWriter.writeJSONB(writer, object, null, null, 0);
                }
            }
            return writer.getBytes();
        }
    }

    /**
     * Converts an object to JSONB bytes with a symbol table
     *
     * @param object the object to convert
     * @param symbolTable the symbol table to use
     * @return the JSONB bytes representation
     */
    static byte[] toBytes(Object object, SymbolTable symbolTable) {
        JSONWriter.Context context = new JSONWriter.Context(defaultObjectWriterProvider);
        try (JSONWriterJSONB writer = new JSONWriterJSONB(
                context,
                symbolTable
        )) {
            if (object == null) {
                writer.writeNull();
            } else {
                writer.setRootObject(object);

                Class<?> valueClass = object.getClass();
                ObjectWriter objectWriter = context.getObjectWriter(valueClass, valueClass);
                objectWriter.writeJSONB(writer, object, null, null, 0);
            }
            return writer.getBytes();
        }
    }

    /**
     * Converts an object to JSONB bytes with a symbol table and features
     *
     * @param object the object to convert
     * @param symbolTable the symbol table to use
     * @param features the JSON writer features to apply
     * @return the JSONB bytes representation
     */
    static byte[] toBytes(Object object, SymbolTable symbolTable, JSONWriter.Feature... features) {
        return toBytes(object, new Context(), symbolTable, features);
    }

    /**
     * Converts an object to JSONB bytes with specified context, symbol table and features
     *
     * @param object the object to convert
     * @param context the JSON writer context
     * @param symbolTable the symbol table to use
     * @param features the JSON writer features to apply
     * @return the JSONB bytes representation
     */
    static byte[] toBytes(Object object, JSONWriter.Context context, SymbolTable symbolTable, JSONWriter.Feature... features) {
        try (JSONWriterJSONB writer = new JSONWriterJSONB(context, symbolTable)) {
            if (object == null) {
                writer.writeNull();
            } else {
                writer.setRootObject(object);

                Class<?> valueClass = object.getClass();

                boolean fieldBased = (context.features & JSONWriter.Feature.FieldBased.mask) != 0;

                ObjectWriter objectWriter = context.provider.getObjectWriter(valueClass, valueClass, fieldBased);
                if ((context.features & JSONWriter.Feature.BeanToArray.mask) != 0) {
                    objectWriter.writeArrayMappingJSONB(writer, object, null, null, 0);
                } else {
                    objectWriter.writeJSONB(writer, object, null, null, 0);
                }
            }
            return writer.getBytes();
        }
    }

    /**
     * Converts an object to JSONB bytes with a symbol table, filters and features
     *
     * @param object the object to convert
     * @param symbolTable the symbol table to use
     * @param filters the filters to apply
     * @param features the JSON writer features to apply
     * @return the JSONB bytes representation
     */
    static byte[] toBytes(Object object, SymbolTable symbolTable, Filter[] filters, JSONWriter.Feature... features) {
        JSONWriter.Context context = new JSONWriter.Context(defaultObjectWriterProvider, features);
        context.configFilter(filters);

        try (JSONWriterJSONB writer = new JSONWriterJSONB(
                context,
                symbolTable
        )) {
            if (object == null) {
                writer.writeNull();
            } else {
                writer.setRootObject(object);

                Class<?> valueClass = object.getClass();

                boolean fieldBased = (context.features & JSONWriter.Feature.FieldBased.mask) != 0;

                ObjectWriter objectWriter = context.provider.getObjectWriter(valueClass, valueClass, fieldBased);
                if ((context.features & JSONWriter.Feature.BeanToArray.mask) != 0) {
                    objectWriter.writeArrayMappingJSONB(writer, object, null, null, 0);
                } else {
                    objectWriter.writeJSONB(writer, object, null, null, 0);
                }
            }
            return writer.getBytes();
        }
    }

    /**
     * Converts an object to JSONB bytes with specified features
     *
     * @param object the object to convert
     * @param features the JSON writer features to apply
     * @return the JSONB bytes representation
     */
    static byte[] toBytes(Object object, JSONWriter.Feature... features) {
        JSONWriter.Context context = new JSONWriter.Context(defaultObjectWriterProvider, features);
        try (JSONWriterJSONB writer = new JSONWriterJSONB(
                context,
                null
        )) {
            if (object == null) {
                writer.writeNull();
            } else {
                writer.rootObject = object;
                writer.path = JSONWriter.Path.ROOT;

                boolean fieldBased = (context.features & JSONWriter.Feature.FieldBased.mask) != 0;

                Class<?> valueClass = object.getClass();
                ObjectWriter objectWriter = context.provider.getObjectWriter(valueClass, valueClass, fieldBased);
                if ((context.features & JSONWriter.Feature.BeanToArray.mask) != 0) {
                    objectWriter.writeArrayMappingJSONB(writer, object, null, null, 0);
                } else {
                    objectWriter.writeJSONB(writer, object, null, null, 0);
                }
            }
            return writer.getBytes();
        }
    }

    /**
     * Creates a symbol table with the specified names
     *
     * @param names the names to include in the symbol table
     * @return the created symbol table
     */
    static SymbolTable symbolTable(String... names) {
        return new SymbolTable(names);
    }

    /**
     * Converts JSONB bytes to a JSON string
     *
     * @param jsonbBytes the JSONB bytes to convert
     * @return the JSON string representation
     */
    static String toJSONString(byte[] jsonbBytes) {
        return new JSONBDump(jsonbBytes, false)
                .toString();
    }

    /**
     * Converts JSONB bytes to a JSON string
     *
     * @param jsonbBytes the JSONB bytes to convert
     * @param raw whether to use raw format
     * @return the JSON string representation
     * @since 2.0.28
     */
    static String toJSONString(byte[] jsonbBytes, boolean raw) {
        return new JSONBDump(jsonbBytes, raw)
                .toString();
    }

    /**
     * Converts JSONB bytes to a JSON string with a symbol table
     *
     * @param jsonbBytes the JSONB bytes to convert
     * @param symbolTable the symbol table to use
     * @return the JSON string representation
     */
    static String toJSONString(byte[] jsonbBytes, SymbolTable symbolTable) {
        return toJSONString(jsonbBytes, symbolTable, false);
    }

    /**
     * Converts JSONB bytes to a JSON string with a symbol table
     *
     * @param jsonbBytes the JSONB bytes to convert
     * @param symbolTable the symbol table to use
     * @param raw whether to use raw format
     * @return the JSON string representation
     */
    static String toJSONString(byte[] jsonbBytes, SymbolTable symbolTable, boolean raw) {
        return new JSONBDump(jsonbBytes, symbolTable, raw)
                .toString();
    }

    /**
     * Writes an object to an output stream as JSONB bytes
     *
     * @param out the output stream to write to
     * @param object the object to write
     * @param features the JSON writer features to apply
     * @return the number of bytes written
     */
    static int writeTo(
            OutputStream out,
            Object object,
            JSONWriter.Feature... features
    ) {
        try (JSONWriterJSONB writer = new JSONWriterJSONB(
                new JSONWriter.Context(JSONFactory.defaultObjectWriterProvider),
                null)
        ) {
            writer.config(features);

            if (object == null) {
                writer.writeNull();
            } else {
                writer.setRootObject(object);

                Class<?> valueClass = object.getClass();
                ObjectWriter objectWriter = writer.getObjectWriter(valueClass, valueClass);
                objectWriter.writeJSONB(writer, object, null, null, 0);
            }

            return writer.flushTo(out);
        } catch (IOException e) {
            throw new JSONException("writeJSONString error", e);
        }
    }

    /**
     * Converts a JSON string to JSONB bytes
     *
     * @param str the JSON string to convert
     * @return the JSONB bytes representation
     */
    static byte[] fromJSONString(String str) {
        return JSONB.toBytes(JSON.parse(str));
    }

    /**
     * Converts JSON bytes to JSONB bytes
     *
     * @param jsonUtf8Bytes the JSON bytes to convert
     * @return the JSONB bytes representation
     */
    static byte[] fromJSONBytes(byte[] jsonUtf8Bytes) {
        JSONReader reader = JSONReader.of(jsonUtf8Bytes);
        ObjectReader objectReader = reader.getObjectReader(Object.class);
        Object object = objectReader.readObject(reader, null, null, 0);
        return JSONB.toBytes(object);
    }

    /**
     * Gets the type name for the specified type byte
     *
     * @param type the type byte
     * @return the type name
     */
    static String typeName(byte type) {
        switch (type) {
            case BC_OBJECT:
                return "OBJECT " + Integer.toString(type);
            case BC_OBJECT_END:
                return "OBJECT_END " + Integer.toString(type);
            case BC_REFERENCE:
                return "REFERENCE " + Integer.toString(type);
            case BC_SYMBOL:
                return "SYMBOL " + Integer.toString(type);
            case BC_NULL:
                return "NULL " + Integer.toString(type);
            case BC_TRUE:
                return "TRUE " + Integer.toString(type);
            case BC_FALSE:
                return "FALSE " + Integer.toString(type);
            case BC_STR_UTF8:
                return "STR_UTF8 " + Integer.toString(type);
            case BC_STR_UTF16:
                return "STR_UTF16 " + Integer.toString(type);
            case BC_STR_UTF16LE:
                return "STR_UTF16LE " + Integer.toString(type);
            case BC_STR_UTF16BE:
                return "STR_UTF16BE " + Integer.toString(type);
            case BC_INT8:
                return "INT8 " + Integer.toString(type);
            case BC_INT16:
                return "INT16 " + Integer.toString(type);
            case BC_INT32:
                return "INT32 " + Integer.toString(type);
            case BC_INT64:
            case BC_INT64_INT:
                return "INT64 " + Integer.toString(type);
            case BC_FLOAT:
            case BC_FLOAT_INT:
                return "FLOAT " + Integer.toString(type);
            case BC_DOUBLE:
            case BC_DOUBLE_LONG:
            case BC_DOUBLE_NUM_0:
            case BC_DOUBLE_NUM_1:
                return "DOUBLE " + Integer.toString(type);
            case BC_BIGINT:
            case BC_BIGINT_LONG:
                return "BIGINT " + Integer.toString(type);
            case BC_DECIMAL:
            case BC_DECIMAL_LONG:
                return "DECIMAL " + Integer.toString(type);
            case Constants.BC_LOCAL_TIME:
                return "LOCAL_TIME " + Integer.toString(type);
            case BC_BINARY:
                return "BINARY " + Integer.toString(type);
            case Constants.BC_LOCAL_DATETIME:
                return "LOCAL_DATETIME " + Integer.toString(type);
            case BC_TIMESTAMP:
                return "TIMESTAMP " + Integer.toString(type);
            case BC_TIMESTAMP_MINUTES:
                return "TIMESTAMP_MINUTES " + Integer.toString(type);
            case BC_TIMESTAMP_SECONDS:
                return "TIMESTAMP_SECONDS " + Integer.toString(type);
            case BC_TIMESTAMP_MILLIS:
                return "TIMESTAMP_MILLIS " + Integer.toString(type);
            case BC_TIMESTAMP_WITH_TIMEZONE:
                return "TIMESTAMP_WITH_TIMEZONE " + Integer.toString(type);
            case Constants.BC_LOCAL_DATE:
                return "LOCAL_DATE " + Integer.toString(type);
            case BC_TYPED_ANY:
                return "TYPED_ANY " + Integer.toString(type);
            default:
                if (type >= BC_ARRAY_FIX_MIN && type <= BC_ARRAY) {
                    return "ARRAY " + Integer.toString(type);
                }

                if (type >= BC_STR_ASCII_FIX_MIN && type <= BC_STR_ASCII) {
                    return "STR_ASCII " + Integer.toString(type);
                }

                if (type >= BC_INT32_NUM_MIN && type <= BC_INT32_NUM_MAX) {
                    return "INT32 " + Integer.toString(type);
                }

                if (type >= BC_INT32_BYTE_MIN && type <= BC_INT32_BYTE_MAX) {
                    return "INT32 " + Integer.toString(type);
                }

                if (type >= BC_INT32_SHORT_MIN && type <= BC_INT32_SHORT_MAX) {
                    return "INT32 " + Integer.toString(type);
                }

                if (type >= BC_INT64_NUM_MIN && type <= BC_INT64_NUM_MAX) {
                    return "INT64 " + Integer.toString(type);
                }

                if (type >= BC_INT64_BYTE_MIN && type <= BC_INT64_BYTE_MAX) {
                    return "INT64 " + Integer.toString(type);
                }

                if (type >= BC_INT64_SHORT_MIN && type <= BC_INT64_SHORT_MAX) {
                    return "INT64 " + Integer.toString(type);
                }

                return Integer.toString(type);
        }
    }

    /**
     * Checks if the specified type is an int32 type
     *
     * @param type the type to check
     * @return true if the type is an int32 type, false otherwise
     */
    static boolean isInt32(int type) {
        return type >= BC_INT32_NUM_MIN && type <= BC_INT32;
    }

    /**
     * Checks if the specified type is an int32 number type
     *
     * @param type the type to check
     * @return true if the type is an int32 number type, false otherwise
     */
    static boolean isInt32Num(int type) {
        return type >= BC_INT32_NUM_MIN && type <= BC_INT32_NUM_MAX;
    }

    /**
     * Checks if the specified type is an int32 byte type
     *
     * @param type the type to check
     * @return true if the type is an int32 byte type, false otherwise
     */
    static boolean isInt32Byte(int type) {
        return (type & 0xF0) == 0x30;
    }

    /**
     * Checks if the specified type is an int32 short type
     *
     * @param type the type to check
     * @return true if the type is an int32 short type, false otherwise
     */
    static boolean isInt32Short(int type) {
        return (type & 0xF8) == 0x40;
    }

    /**
     * Checks if the specified type is an int64 number type
     *
     * @param type the type to check
     * @return true if the type is an int64 number type, false otherwise
     */
    static boolean isInt64Num(int type) {
        return type >= BC_INT64_NUM_MIN && type <= BC_INT64_NUM_MAX;
    }

    /**
     * Checks if the specified type is an int64 byte type
     *
     * @param type the type to check
     * @return true if the type is an int64 byte type, false otherwise
     */
    static boolean isInt64Byte(int type) {
        return ((type - BC_INT64_BYTE_MIN) & 0xF0) == 0;
    }

    /**
     * Checks if the specified type is an int64 short type
     *
     * @param type the type to check
     * @return true if the type is an int64 short type, false otherwise
     */
    static boolean isInt64Short(int type) {
        return (type & 0xF8) == 0xC0;
    }

    /**
     * Checks if the specified integer value can be represented as an int32 byte value
     *
     * @param i the integer value to check
     * @return true if the value can be represented as an int32 byte value, false otherwise
     */
    static boolean isInt32ByteValue(int i) {
        return ((i + 2048) & ~0xFFF) != 0;
    }

    /**
     * Checks if the specified integer value is within the int32 byte value range
     *
     * @param i the integer value to check
     * @return true if the value is within the int32 byte value range, false otherwise
     */
    static boolean isInt32ByteValue1(int i) {
        return i >= INT32_BYTE_MIN && i <= INT32_BYTE_MAX;
    }

    /**
     * IO utility methods for JSONB serialization
     */
    interface IO {
        /**
         * Calculates the capacity needed for an enum
         *
         * @param e the enum value
         * @param features the features to apply
         * @return the capacity needed
         */
        static int enumCapacity(Enum e, long features) {
            if ((features & (MASK_WRITE_ENUM_USING_TO_STRING | MASK_WRITE_ENUMS_USING_NAME)) != 0) {
                return stringCapacity((features & WriteEnumUsingToString.mask) != 0
                        ? e.toString()
                        : e.name());
            }
            return 5;
        }

        /**
         * Writes an enum value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param e the enum value to write
         * @param features the features to apply
         * @return the new offset
         */
        static int writeEnum(byte[] bytes, int off, Enum e, long features) {
            if ((features & (MASK_WRITE_ENUM_USING_TO_STRING | MASK_WRITE_ENUMS_USING_NAME)) != 0) {
                return writeString(bytes, off,
                        (features & WriteEnumUsingToString.mask) != 0
                                ? e.toString()
                                : e.name()
                );
            } else {
                return JSONB.IO.writeInt32(bytes, off, e.ordinal());
            }
        }

        /**
         * Writes a Boolean value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the Boolean value to write
         * @return the new offset
         */
        static int writeBoolean(byte[] bytes, int off, Boolean value) {
            bytes[off] = value == null ? BC_NULL : value ? BC_TRUE : BC_FALSE;
            return off + 1;
        }

        /**
         * Writes a boolean value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the boolean value to write
         * @return the new offset
         */
        static int writeBoolean(byte[] bytes, int off, boolean value) {
            bytes[off] = value ? BC_TRUE : BC_FALSE;
            return off + 1;
        }

        /**
         * Writes a boolean array to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param values the boolean array to write
         * @return the new offset
         */
        static int writeBoolean(byte[] bytes, int off, boolean[] values) {
            if (values == null) {
                bytes[off] = BC_NULL;
                return off + 1;
            }
            off = startArray(bytes, off, values.length);
            for (int i = 0; i < values.length; i++) {
                bytes[off + i] = values[i] ? BC_TRUE : BC_FALSE;
            }
            return off + values.length;
        }

        /**
         * Writes a Float value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the Float value to write
         * @param features the features to apply
         * @return the new offset
         */
        static int writeFloat(byte[] bytes, int off, Float value, long features) {
            float floatValue;
            if (value == null) {
                if ((features & (MASK_NULL_AS_DEFAULT_VALUE | MASK_WRITE_NULL_NUMBER_AS_ZERO)) == 0) {
                    bytes[off] = BC_NULL;
                    return off + 1;
                }
                floatValue = 0;
            } else {
                floatValue = value;
            }
            return IO.writeFloat(bytes, off, floatValue);
        }

        /**
         * Writes a float array to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param values the float array to write
         * @return the new offset
         */
        static int writeFloat(byte[] bytes, int off, float[] values) {
            if (values == null) {
                bytes[off] = BC_NULL;
                return off + 1;
            }
            off = startArray(bytes, off, values.length);
            for (float value : values) {
                off = IO.writeFloat(bytes, off, value);
            }
            return off;
        }

        /**
         * Writes a float value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the float value to write
         * @return the new offset
         */
        static int writeFloat(byte[] bytes, int off, float value) {
            int intValue = (int) value;
            if (intValue == value && ((intValue + 0x40000) & ~0x7ffff) == 0) {
                bytes[off] = BC_FLOAT_INT;
                return IO.writeInt32(bytes, off + 1, intValue);
            }

            bytes[off] = BC_FLOAT;
            IOUtils.putIntBE(bytes, off + 1, Float.floatToIntBits(value));
            return off + 5;
        }

        /**
         * Writes a Double value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the Double value to write
         * @param features the features to apply
         * @return the new offset
         */
        static int writeDouble(byte[] bytes, int off, Double value, long features) {
            if (value == null) {
                bytes[off] = (features & (MASK_NULL_AS_DEFAULT_VALUE | MASK_WRITE_NULL_NUMBER_AS_ZERO)) == 0
                        ? BC_NULL
                        : BC_DOUBLE_NUM_0;
                bytes[off] = (features & (MASK_NULL_AS_DEFAULT_VALUE | MASK_WRITE_NULL_NUMBER_AS_ZERO)) == 0 ? BC_NULL : BC_DOUBLE_NUM_0;
                return off + 1;
            }
            return IO.writeDouble(bytes, off, value);
        }

        /**
         * Writes a double value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the double value to write
         * @return the new offset
         */
        static int writeDouble(byte[] bytes, int off, double value) {
            if (value == 0 || value == 1) {
                bytes[off] = value == 0 ? BC_DOUBLE_NUM_0 : BC_DOUBLE_NUM_1;
                return off + 1;
            }

            if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) {
                long longValue = (long) value;
                if (longValue == value) {
                    bytes[off] = BC_DOUBLE_LONG;
                    return IO.writeInt64(bytes, off + 1, longValue);
                }
            }

            bytes[off] = BC_DOUBLE;
            IOUtils.putLongBE(bytes, off + 1, Double.doubleToLongBits(value));
            return off + 9;
        }

        /**
         * Writes a double array to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param values the double array to write
         * @return the new offset
         */
        static int writeDouble(byte[] bytes, int off, double[] values) {
            if (values == null) {
                bytes[off] = BC_NULL;
                return off + 1;
            }
            off = startArray(bytes, off, values.length);
            for (double value : values) {
                off = writeDouble(bytes, off, value);
            }
            return off;
        }

        /**
         * Writes a Byte value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param val the Byte value to write
         * @param features the features to apply
         * @return the new offset
         */
        static int writeInt8(byte[] bytes, int off, Byte val, long features) {
            if (val == null) {
                bytes[off] = (features & (MASK_NULL_AS_DEFAULT_VALUE | MASK_WRITE_NULL_NUMBER_AS_ZERO)) == 0 ? BC_NULL : 0;
                return off + 1;
            }
            putShortLE(bytes, off, (short) ((val << 8) | (BC_INT8 & 0xFF)));
            return off + 2;
        }

        /**
         * Writes a byte value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param val the byte value to write
         * @return the new offset
         */
        static int writeInt8(byte[] bytes, int off, byte val) {
            putShortLE(bytes, off, (short) ((val << 8) | (BC_INT8 & 0xFF)));
            return off + 2;
        }

        /**
         * Writes a Short value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param val the Short value to write
         * @param features the features to apply
         * @return the new offset
         */
        static int writeInt16(byte[] bytes, int off, Short val, long features) {
            if (val == null) {
                bytes[off] = (features & (MASK_NULL_AS_DEFAULT_VALUE | MASK_WRITE_NULL_NUMBER_AS_ZERO)) == 0 ? BC_NULL : 0;
                return off + 1;
            }
            bytes[off] = BC_INT16;
            putShortBE(bytes, off + 1, val);
            return off + 3;
        }

        /**
         * Writes a short value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param val the short value to write
         * @return the new offset
         */
        static int writeInt16(byte[] bytes, int off, short val) {
            bytes[off] = BC_INT16;
            putShortBE(bytes, off + 1, val);
            return off + 3;
        }

        /**
         * Writes an Integer value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the Integer value to write
         * @param features the features to apply
         * @return the new offset
         */
        static int writeInt32(byte[] bytes, int off, Integer value, long features) {
            if (value == null) {
                bytes[off] = (features & (MASK_NULL_AS_DEFAULT_VALUE | MASK_WRITE_NULL_NUMBER_AS_ZERO)) == 0 ? BC_NULL : 0;
                return off + 1;
            }
            return IO.writeInt32(bytes, off, value);
        }

        /**
         * Writes a string with a symbol table to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param str the string to write
         * @param symbolTable the symbol table to use
         * @return the new offset
         */
        static int writeSymbol(byte[] bytes, int off, String str, SymbolTable symbolTable) {
            if (str == null) {
                bytes[off] = BC_NULL;
                return off + 1;
            }
            int ordinal = symbolTable.getOrdinal(str);
            if (ordinal >= 0) {
                bytes[off] = BC_STR_ASCII;
                return writeInt32(bytes, off + 1, -ordinal);
            }
            return writeString(bytes, off, str);
        }

        /**
         * Writes a symbol to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param symbol the symbol to write
         * @return the new offset
         */
        static int writeSymbol(byte[] bytes, int off, int symbol) {
            bytes[off++] = BC_SYMBOL;

            if (symbol >= BC_INT32_NUM_MIN && symbol <= BC_INT32_NUM_MAX) {
                bytes[off++] = (byte) symbol;
            } else if (symbol >= INT32_BYTE_MIN && symbol <= INT32_BYTE_MAX) {
                putShortBE(bytes, off, (short) ((BC_INT32_BYTE_ZERO << 8) + symbol));
                off += 2;
            } else {
                off = JSONB.IO.writeInt32(bytes, off, symbol);
            }
            return off;
        }

        /**
         * Checks and writes a type name to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param object the object to check
         * @param fieldClass the field class
         * @param jsonWriter the JSON writer
         * @return the new offset
         */
        static int checkAndWriteTypeName(byte[] bytes, int off, Object object, Class<?> fieldClass, JSONWriter jsonWriter) {
            long features = jsonWriter.getFeatures();
            Class<?> objectClass;
            if ((features & WriteClassName.mask) == 0
                    || object == null
                    || (objectClass = object.getClass()) == fieldClass
                    || ((features & NotWriteHashMapArrayListClassName.mask) != 0 && (objectClass == HashMap.class || objectClass == ArrayList.class))
                    || ((features & NotWriteRootClassName.mask) != 0 && object == jsonWriter.rootObject)
            ) {
                return off;
            }

            return writeTypeName(bytes, off, TypeUtils.getTypeName(objectClass), jsonWriter);
        }

        /**
         * Writes a type name to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param typeName the type name to write
         * @param jsonWriter the JSON writer
         * @return the new offset
         */
        static int writeTypeName(byte[] bytes, int off, String typeName, JSONWriter jsonWriter) {
            JSONWriterJSONB jsonWriterJSONB = (JSONWriterJSONB) jsonWriter;
            SymbolTable symbolTable = jsonWriter.symbolTable;
            bytes[off++] = BC_TYPED_ANY;

            long hash = Fnv.hashCode64(typeName);

            int symbol = -1;
            if (symbolTable != null) {
                symbol = symbolTable.getOrdinalByHashCode(hash);
                if (symbol == -1 && jsonWriterJSONB.symbols != null) {
                    symbol = jsonWriterJSONB.symbols.get(hash);
                }
            } else if (jsonWriterJSONB.symbols != null) {
                symbol = jsonWriterJSONB.symbols.get(hash);
            }

            if (symbol == -1) {
                if (jsonWriterJSONB.symbols == null) {
                    jsonWriterJSONB.symbols = new TLongIntHashMap();
                }
                jsonWriterJSONB.symbols.put(hash, symbol = jsonWriterJSONB.symbolIndex++);
            } else {
                return JSONB.IO.writeInt32(bytes, off, symbol);
            }

            off = writeString(bytes, off, typeName);
            return writeInt32(bytes, off, symbol);
        }

        /**
         * Writes an integer value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the integer value to write
         * @return the new offset
         */
        static int writeInt32(byte[] bytes, int off, int value) {
            if (((value + 0x10) & ~0x3f) == 0) {
                bytes[off++] = (byte) value;
            } else if (((value + 0x800) & ~0xfff) == 0) {
                putShortBE(bytes, off, (short) ((BC_INT32_BYTE_ZERO << 8) + value));
                off += 2;
            } else if (((value + 0x40000) & ~0x7ffff) == 0) {
                bytes[off] = (byte) (BC_INT32_SHORT_ZERO + (value >> 16));
                putShortBE(bytes, off + 1, (short) value);
                off += 3;
            } else {
                bytes[off] = BC_INT32;
                putIntBE(bytes, off + 1, value);
                off += 5;
            }
            return off;
        }

        /**
         * Writes a collection of Long values to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param values the collection of Long values to write
         * @param features the features to apply
         * @return the new offset
         */
        static int writeInt64(byte[] bytes, int off, Collection<Long> values, long features) {
            if (values == null) {
                bytes[off] = (features & WRITE_ARRAY_NULL_MASK) != 0 ? BC_ARRAY_FIX_MIN : BC_NULL;
                return off + 1;
            }
            int size = values.size();
            off = startArray(bytes, off, size);
            for (Long value : values) {
                off = writeInt64(bytes, off, value, features);
            }
            return off;
        }

        /**
         * Writes a Long value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the Long value to write
         * @param features the features to apply
         * @return the new offset
         */
        static int writeInt64(byte[] bytes, int off, Long value, long features) {
            if (value == null) {
                bytes[off] = (features & (MASK_NULL_AS_DEFAULT_VALUE | MASK_WRITE_NULL_NUMBER_AS_ZERO)) == 0
                        ? BC_NULL
                        : (byte) (BC_INT64_NUM_MIN - INT64_NUM_LOW_VALUE);
                return off + 1;
            }
            return IO.writeInt64(bytes, off, value);
        }

        /**
         * Writes a long value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the long value to write
         * @return the new offset
         */
        static int writeInt64(byte[] bytes, int off, long value) {
            if (value >= INT64_NUM_LOW_VALUE && value <= INT64_NUM_HIGH_VALUE) {
                bytes[off++] = (byte) (BC_INT64_NUM_MIN + (value - INT64_NUM_LOW_VALUE));
            } else if (((value + 0x800) & ~0xfffL) == 0) {
                putShortBE(bytes, off, (short) ((BC_INT64_BYTE_ZERO << 8) + value));
                off += 2;
            } else if (((value + 0x40000) & ~0x7ffffL) == 0) {
                bytes[off] = (byte) (BC_INT64_SHORT_ZERO + (value >> 16));
                putShortBE(bytes, off + 1, (short) value);
                off += 3;
            } else if ((((value + 0x80000000L) & ~0xffffffffL) == 0)) {
                bytes[off] = BC_INT64_INT;
                putIntBE(bytes, off + 1, (int) value);
                off += 5;
            } else {
                bytes[off] = BC_INT64;
                putLongBE(bytes, off + 1, value);
                off += 9;
            }
            return off;
        }

        /**
         * Starts an array in a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param size the size of the array
         * @return the new offset
         */
        static int startArray(byte[] bytes, int off, int size) {
            boolean tinyInt = size <= ARRAY_FIX_LEN;
            bytes[off++] = tinyInt ? (byte) (BC_ARRAY_FIX_MIN + size) : BC_ARRAY;
            if (!tinyInt) {
                off = writeInt32(bytes, off, size);
            }
            return off;
        }

        /**
         * Writes a collection of strings to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param strings the collection of strings to write
         * @param features the features to apply
         * @return the new offset
         */
        static int writeString(byte[] bytes, int off, Collection<String> strings, long features) {
            if (strings == null) {
                bytes[off] = (features & WRITE_ARRAY_NULL_MASK) != 0 ? BC_ARRAY_FIX_MIN : BC_NULL;
                return off + 1;
            }
            int size = strings.size();
            off = startArray(bytes, off, size);
            for (String string : strings) {
                off = writeString(bytes, off, string);
            }
            return off;
        }

        /**
         * Writes a string array to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param strings the string array to write
         * @param features the features to apply
         * @return the new offset
         */
        static int writeString(byte[] bytes, int off, String[] strings, long features) {
            if (strings == null) {
                bytes[off] = (features & WRITE_ARRAY_NULL_MASK) != 0 ? BC_ARRAY_FIX_MIN : BC_NULL;
                return off + 1;
            }
            int size = strings.length;
            off = startArray(bytes, off, size);
            for (String string : strings) {
                off = writeString(bytes, off, string);
            }
            return off;
        }

        /**
         * Writes a string to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param str the string to write
         * @return the new offset
         */
        static int writeString(byte[] bytes, int off, String str) {
            if (str == null) {
                bytes[off] = BC_NULL;
                return off + 1;
            }
            if (STRING_CODER != null && STRING_VALUE != null) {
                int coder = STRING_CODER.applyAsInt(str);
                byte[] value = STRING_VALUE.apply(str);
                if (coder == 0) {
                    return writeStringLatin1(bytes, off, value);
                } else {
                    return writeStringUTF16(bytes, off, value);
                }
            } else {
                return writeString(bytes, off, JDKUtils.getCharArray(str));
            }
        }

        /**
         * Writes a UTF-16 string to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the UTF-16 byte array to write
         * @return the new offset
         */
        static int writeStringUTF16(byte[] bytes, int off, byte[] value) {
            final int strlen = value.length;
            bytes[off] = JDKUtils.BIG_ENDIAN ? BC_STR_UTF16BE : BC_STR_UTF16LE;
            off = JSONB.IO.writeInt32(bytes, off + 1, strlen);
            System.arraycopy(value, 0, bytes, off, strlen);
            return off + strlen;
        }

        /**
         * Writes a Latin-1 string to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the Latin-1 byte array to write
         * @return the new offset
         */
        static int writeStringLatin1(byte[] bytes, int off, byte[] value) {
            int strlen = value.length;
            if (strlen <= STR_ASCII_FIX_LEN) {
                bytes[off++] = (byte) (strlen + BC_STR_ASCII_FIX_MIN);
            } else if (strlen <= INT32_BYTE_MAX) {
                off = putStringSizeSmall(bytes, off, strlen);
            } else {
                off = putStringSizeLarge(bytes, off, strlen);
            }
            System.arraycopy(value, 0, bytes, off, value.length);
            return off + strlen;
        }

        /**
         * Calculates the capacity needed for a collection of strings
         *
         * @param strings the collection of strings
         * @return the capacity needed
         */
        static int stringCapacity(Collection<String> strings) {
            if (strings == null) {
                return 1;
            }
            int size = stringCapacity(strings.getClass().getName()) + 7;
            for (String string : strings) {
                size += stringCapacity(string);
            }
            return size;
        }

        /**
         * Calculates the capacity needed for a string array
         *
         * @param strings the string array
         * @return the capacity needed
         */
        static int stringCapacity(String[] strings) {
            if (strings == null) {
                return 1;
            }
            int size = 6;
            for (String string : strings) {
                size += stringCapacity(string);
            }
            return size;
        }

        /**
         * Calculates the capacity needed for a collection of Long values
         *
         * @param values the collection of Long values
         * @return the capacity needed
         */
        static int int64Capacity(Collection<Long> values) {
            if (values == null) {
                return 1;
            }
            return stringCapacity(values.getClass().getName())
                    + 7
                    + values.size() * 9;
        }

        /**
         * Calculates the capacity needed for a string
         *
         * @param str the string
         * @return the capacity needed
         */
        static int stringCapacity(String str) {
            if (str == null) {
                return 0;
            }

            int strlen = str.length();
            if (STRING_CODER != null && STRING_VALUE != null) {
                return (strlen << STRING_CODER.applyAsInt(str)) + 6;
            }

            return strlen * 3 + 6;
        }

        /**
         * Puts a small string size to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param val the size value to write
         * @return the new offset
         */
        static int putStringSizeSmall(byte[] bytes, int off, int val) {
            bytes[off] = BC_STR_ASCII;
            putShortBE(bytes, off + 1, (short) ((BC_INT32_BYTE_ZERO << 8) + val));
            return off + 3;
        }

        /**
         * Puts a large string size to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param strlen the size value to write
         * @return the new offset
         */
        static int putStringSizeLarge(byte[] bytes, int off, int strlen) {
            if (strlen <= INT32_SHORT_MAX) {
                putIntBE(bytes, off, (BC_STR_ASCII << 24) + (BC_INT32_SHORT_ZERO << 16) + strlen);
                return off + 4;
            }

            putShortBE(bytes, off, (short) ((BC_STR_ASCII << 8) | BC_INT32));
            putIntBE(bytes, off + 2, strlen);
            return off + 6;
        }

        /**
         * Writes a character array to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param chars the character array to write
         * @return the new offset
         */
        static int writeString(byte[] bytes, int off, char[] chars) {
            return writeString(bytes, off, chars, 0, chars.length);
        }

        /**
         * Writes a character array with offset and length to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param chars the character array to write
         * @param coff the offset in the character array
         * @param strlen the length of characters to write
         * @return the new offset
         */
        static int writeString(byte[] bytes, int off, char[] chars, int coff, int strlen) {
            int start = off;
            boolean ascii = true;
            if (strlen < STR_ASCII_FIX_LEN) {
                bytes[off++] = (byte) (strlen + BC_STR_ASCII_FIX_MIN);
                for (int i = coff, end = coff + strlen; i < end; i++) {
                    char ch = chars[i];
                    if (ch > 0x00FF) {
                        ascii = false;
                        break;
                    }
                    bytes[off++] = (byte) ch;
                }

                if (ascii) {
                    return off;
                }

                off = start;
            } else {
                ascii = isLatin1(chars, coff, strlen);
            }

            if (ascii) {
                off = writeStringLatin1(bytes, off, chars, coff, strlen);
            } else {
                off = writeUTF8(bytes, off, chars, coff, strlen);
            }
            return off;
        }

        /**
         * Writes a Latin-1 character array to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param chars the character array to write
         * @param coff the offset in the character array
         * @param strlen the length of characters to write
         * @return the new offset
         */
        static int writeStringLatin1(byte[] bytes, int off, char[] chars, int coff, int strlen) {
            if (strlen <= STR_ASCII_FIX_LEN) {
                bytes[off++] = (byte) (strlen + BC_STR_ASCII_FIX_MIN);
            } else {
                bytes[off] = BC_STR_ASCII;
                if (strlen <= INT32_BYTE_MAX) {
                    putShortBE(bytes, off + 1, (short) ((BC_INT32_BYTE_ZERO << 8) + strlen));
                    off += 3;
                } else {
                    off = JSONB.IO.writeInt32(bytes, off + 1, strlen);
                }
            }
            for (int i = 0; i < strlen; i++) {
                bytes[off++] = (byte) chars[coff + i];
            }
            return off;
        }

        /**
         * Writes a UTF-8 character array to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param chars the character array to write
         * @param coff the offset in the character array
         * @param strlen the length of characters to write
         * @return the new offset
         */
        static int writeUTF8(byte[] bytes, int off, char[] chars, int coff, int strlen) {
            int maxSize = strlen * 3;
            int lenByteCnt = sizeOfInt(maxSize);
            int result = IOUtils.encodeUTF8(chars, coff, strlen, bytes, off + lenByteCnt + 1);

            int utf8len = result - off - lenByteCnt - 1;
            int utf8lenByteCnt = sizeOfInt(utf8len);
            if (lenByteCnt != utf8lenByteCnt) {
                System.arraycopy(bytes, off + lenByteCnt + 1, bytes, off + utf8lenByteCnt + 1, utf8len);
            }
            bytes[off] = BC_STR_UTF8;
            return JSONB.IO.writeInt32(bytes, off + 1, utf8len) + utf8len;
        }

        /**
         * Calculates the size needed for an integer value
         *
         * @param i the integer value
         * @return the size needed
         */
        static int sizeOfInt(int i) {
            if (i >= BC_INT32_NUM_MIN && i <= BC_INT32_NUM_MAX) {
                return 1;
            }

            if (i >= INT32_BYTE_MIN && i <= INT32_BYTE_MAX) {
                return 2;
            }

            if (i >= INT32_SHORT_MIN && i <= INT32_SHORT_MAX) {
                return 3;
            }

            return 5;
        }

        /**
         * Writes a UUID value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the UUID value to write
         * @return the new offset
         */
        static int writeUUID(byte[] bytes, int off, UUID value) {
            if (value == null) {
                bytes[off] = BC_NULL;
                return off + 1;
            }
            putShortLE(bytes, off, (short) ((BC_BINARY & 0xFF) | ((BC_INT32_NUM_16 & 0xFF) << 8)));
            putLongBE(bytes, off + 2, value.getMostSignificantBits());
            putLongBE(bytes, off + 10, value.getLeastSignificantBits());
            return off + 18;
        }

        /**
         * Writes an Instant value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the Instant value to write
         * @return the new offset
         */
        static int writeInstant(byte[] bytes, int off, Instant value) {
            if (value == null) {
                bytes[off] = BC_NULL;
                return off + 1;
            }
            bytes[off] = BC_TIMESTAMP;
            off = JSONB.IO.writeInt64(bytes, off + 1, value.getEpochSecond());
            return JSONB.IO.writeInt32(bytes, off, value.getNano());
        }

        /**
         * Writes a LocalDate value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the LocalDate value to write
         * @return the new offset
         */
        static int writeLocalDate(byte[] bytes, int off, LocalDate value) {
            if (value == null) {
                bytes[off] = BC_NULL;
                return off + 1;
            }
            bytes[off] = BC_LOCAL_DATE;
            int year = value.getYear();
            putIntBE(bytes, off + 1, (year << 16) | (value.getMonthValue() << 8) | value.getDayOfMonth());
            return off + 5;
        }

        /**
         * Writes a LocalTime value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the LocalTime value to write
         * @return the new offset
         */
        static int writeLocalTime(byte[] bytes, int off, LocalTime value) {
            if (value == null) {
                bytes[off] = BC_NULL;
                return off + 1;
            }
            putIntBE(bytes,
                    off,
                    (BC_LOCAL_TIME << 24) | (value.getHour() << 16) | (value.getMinute() << 8) | value.getSecond());
            return JSONB.IO.writeInt32(bytes, off + 4, value.getNano());
        }

        /**
         * Writes a LocalDateTime value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the LocalDateTime value to write
         * @return the new offset
         */
        static int writeLocalDateTime(byte[] bytes, int off, LocalDateTime value) {
            if (value == null) {
                bytes[off] = BC_NULL;
                return off + 1;
            }
            putIntBE(bytes,
                    off,
                    (BC_LOCAL_DATETIME << 24) | (value.getYear() << 8) | value.getMonthValue());
            putIntBE(bytes,
                    off + 4,
                    (value.getDayOfMonth() << 24)
                            | (value.getHour() << 16)
                            | (value.getMinute() << 8)
                            | value.getSecond());
            return writeInt32(bytes, off + 8, value.getNano());
        }

        /**
         * Writes an OffsetDateTime value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the OffsetDateTime value to write
         * @return the new offset
         */
        static int writeOffsetDateTime(byte[] bytes, int off, OffsetDateTime value) {
            if (value == null) {
                bytes[off] = BC_NULL;
                return off + 1;
            }
            putIntBE(bytes,
                    off,
                    (BC_TIMESTAMP_WITH_TIMEZONE << 24) | (value.getYear() << 8) | value.getMonthValue());
            putIntBE(bytes,
                    off + 4,
                    (value.getDayOfMonth() << 24)
                            | (value.getHour() << 16)
                            | (value.getMinute() << 8)
                            | value.getSecond());

            off = writeInt32(bytes, off + 8, value.getNano());

            String zoneIdStr = value.getOffset().getId();
            int strlen = zoneIdStr.length();
            bytes[off] = (byte) (strlen + BC_STR_ASCII_FIX_MIN);
            zoneIdStr.getBytes(0, strlen, bytes, off + 1);
            return off + strlen + 1;
        }

        /**
         * Writes an OffsetTime value to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param value the OffsetTime value to write
         * @return the new offset
         */
        static int writeOffsetTime(byte[] bytes, int off, OffsetTime value) {
            if (value == null) {
                bytes[off] = BC_NULL;
                return off + 1;
            }

            int year = 1970, month = 1, dayOfMonth = 1;
            putIntBE(bytes,
                    off,
                    (BC_TIMESTAMP_WITH_TIMEZONE << 24) | (year << 8) | month);
            putIntBE(bytes,
                    off + 4,
                    (dayOfMonth << 24)
                            | (value.getHour() << 16)
                            | (value.getMinute() << 8)
                            | value.getSecond());

            off = writeInt32(bytes, off + 8, value.getNano());

            String zoneIdStr = value.getOffset().getId();
            int strlen = zoneIdStr.length();
            bytes[off] = (byte) (strlen + BC_STR_ASCII_FIX_MIN);
            zoneIdStr.getBytes(0, strlen, bytes, off + 1);
            return off + strlen + 1;
        }

        /**
         * Writes a reference to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param path the reference path
         * @param jsonWriter the JSON writer
         * @return the new offset
         */
        static int writeReference(byte[] bytes, int off, String path, JSONWriter jsonWriter) {
            if (jsonWriter.lastReference == path) {
                path = "#-1";
            } else {
                jsonWriter.lastReference = path;
            }
            bytes[off] = BC_REFERENCE;
            return writeString(bytes, off + 1, path);
        }

        /**
         * Writes a raw name to a byte array
         *
         * @param bytes the byte array to write to
         * @param off the offset in the byte array
         * @param name the name byte array to write
         * @param nameHash the name hash
         * @param jsonWriter the JSON writer
         * @return the new offset
         */
        static int writeNameRaw(byte[] bytes, int off, byte[] name, long nameHash, JSONWriter jsonWriter) {
            SymbolTable symbolTable = jsonWriter.symbolTable;
            JSONWriterJSONB jsonWriterJSONB = (JSONWriterJSONB) jsonWriter;
            int symbol;
            if (symbolTable == null
                    || (symbol = symbolTable.getOrdinalByHashCode(nameHash)) == -1
            ) {
                if ((jsonWriter.context.features & WriteNameAsSymbol.mask) == 0) {
                    System.arraycopy(name, 0, bytes, off, name.length);
                    return off + name.length;
                }

                boolean symbolExists = false;
                if (jsonWriterJSONB.symbols != null) {
                    if ((symbol = jsonWriterJSONB.symbols.putIfAbsent(nameHash, jsonWriterJSONB.symbolIndex)) != jsonWriterJSONB.symbolIndex) {
                        symbolExists = true;
                    } else {
                        jsonWriterJSONB.symbolIndex++;
                    }
                } else {
                    (jsonWriterJSONB.symbols = new TLongIntHashMap())
                            .put(nameHash, symbol = jsonWriterJSONB.symbolIndex++);
                }

                if (!symbolExists) {
                    bytes[off++] = BC_SYMBOL;
                    System.arraycopy(name, 0, bytes, off, name.length);
                    off += name.length;

                    if (symbol >= BC_INT32_NUM_MIN && symbol <= BC_INT32_NUM_MAX) {
                        bytes[off++] = (byte) symbol;
                    } else {
                        off = JSONB.IO.writeInt32(bytes, off, symbol);
                    }
                    return off;
                }
                symbol = -symbol;
            }

            bytes[off++] = BC_SYMBOL;
            int intValue = -symbol;
            if (intValue >= BC_INT32_NUM_MIN && intValue <= BC_INT32_NUM_MAX) {
                bytes[off++] = (byte) intValue;
            } else {
                off = JSONB.IO.writeInt32(bytes, off, intValue);
            }
            return off;
        }
    }
}