Skip to content

Conversation

@jujn
Copy link
Contributor

@jujn jujn commented Dec 16, 2025

…for base types, for issue #3870

What this PR does / why we need it?

是否可以提供如下功能:
对基础类型提供内置 NonStringValueAsString 序列化器;
方便用户全局注册。

尝试了一种实现方式(草稿,感觉不太好,无法避免装箱拆箱),跑了 BenchMark 程序和结果如下:

点击查看 benchmark 代码
package com.alibaba.fastjson2.benchmark;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.writer.ObjectWriterAsString;
import com.alibaba.fastjson2.writer.ObjectWriterProvider;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@Fork(3) 
@Warmup(iterations = 5, time = 5) 
@Measurement(iterations = 5, time = 5)
public class AsStringBenchmark {

    public static class PrimitiveBean {
        public int vInt;
        public long vLong;
        public float vFloat;
        public double vDouble;
        public short vShort;
        public byte vByte;
        public boolean vBoolean;
        public char vChar;
        public Integer vInteger;
        public Long vLongWrapper;
        public Float vFloatWrapper;
        public Double vDoubleWrapper;
        public Short vShortWrapper;
        public Byte vByteWrapper;
        public Boolean vBooleanWrapper;
        public Character vCharWrapper;
        public BigInteger vBigInteger;
        public BigDecimal vBigDecimal;
        public int[] vIntArray;
        public long[] vLongArray;
        public float[] vFloatArray;
        public double[] vDoubleArray;
        public short[] vShortArray;
        public byte[] vByteArray;
        public boolean[] vBooleanArray;

        public PrimitiveBean() {
            this.vInt = 10086;
            this.vLong = 1678888888888L;
            this.vFloat = 123.456f;
            this.vDouble = 3.1415926;
            this.vShort = (short) 123;
            this.vByte = (byte) 127;
            this.vBoolean = true;
            this.vChar = 'A';

            this.vInteger = 999;
            this.vLongWrapper = 54321L;
            this.vFloatWrapper = 67.89f;
            this.vDoubleWrapper = 0.9999;
            this.vShortWrapper = (short) 456;
            this.vByteWrapper = (byte) -1;
            this.vBooleanWrapper = Boolean.FALSE;
            this.vCharWrapper = 'Z';

            this.vBigInteger = new BigInteger("12345678901234567890");
            this.vBigDecimal = new BigDecimal("12345.67890123456789");

            this.vIntArray = new int[]{1, 2, 3, 4, 5};
            this.vLongArray = new long[]{100L, 200L, 300L, 400L, 500L};
            this.vFloatArray = new float[]{1.1f, 2.2f, 3.3f, 4.4f, 5.5f};
            this.vDoubleArray = new double[]{1.11, 2.22, 3.33, 4.44, 5.55};
            this.vShortArray = new short[]{10, 20, 30, 40, 50};
            this.vByteArray = new byte[]{0, 1, 2, 3, 4};
            this.vBooleanArray = new boolean[]{true, false, true, true, false};
        }
    }

    private PrimitiveBean bean;
    private JSONWriter.Context defaultContext;
    private JSONWriter.Context asStringContext;

    @Setup
    public void setup() {
        bean = new PrimitiveBean();

        ObjectWriterProvider customProvider0 = new ObjectWriterProvider();
        defaultContext = new JSONWriter.Context(customProvider0);
        defaultContext.config(JSONWriter.Feature.WriteNonStringValueAsString);


        ObjectWriterProvider customProvider = new ObjectWriterProvider();

        customProvider.register(int.class, ObjectWriterAsString.OF_INT_VALUE);
        customProvider.register(long.class, ObjectWriterAsString.OF_LONG_VALUE);
        customProvider.register(float.class, ObjectWriterAsString.OF_FLOAT_VALUE);
        customProvider.register(double.class, ObjectWriterAsString.OF_DOUBLE_VALUE);
        customProvider.register(short.class, ObjectWriterAsString.OF_SHORT_VALUE);
        customProvider.register(byte.class, ObjectWriterAsString.OF_BYTE_VALUE);
        customProvider.register(boolean.class, ObjectWriterAsString.OF_BOOLEAN_VALUE);
        customProvider.register(char.class, ObjectWriterAsString.OF_CHAR_VALUE);

        customProvider.register(Integer.class, ObjectWriterAsString.OF_INTEGER);
        customProvider.register(Long.class, ObjectWriterAsString.OF_LONG);
        customProvider.register(Float.class, ObjectWriterAsString.OF_FLOAT);
        customProvider.register(Double.class, ObjectWriterAsString.OF_DOUBLE);
        customProvider.register(Short.class, ObjectWriterAsString.OF_SHORT);
        customProvider.register(Byte.class, ObjectWriterAsString.OF_BYTE);
        customProvider.register(Boolean.class, ObjectWriterAsString.OF_BOOLEAN);
        customProvider.register(Character.class, ObjectWriterAsString.OF_CHARACTER);

        customProvider.register(BigInteger.class, ObjectWriterAsString.OF_BIG_INTEGER);
        customProvider.register(BigDecimal.class, ObjectWriterAsString.OF_BIG_DECIMAL);

        customProvider.register(int[].class, ObjectWriterAsString.OF_INT_ARRAY);
        customProvider.register(long[].class, ObjectWriterAsString.OF_LONG_ARRAY);
        customProvider.register(float[].class, ObjectWriterAsString.OF_FLOAT_ARRAY);
        customProvider.register(double[].class, ObjectWriterAsString.OF_DOUBLE_ARRAY);
        customProvider.register(short[].class, ObjectWriterAsString.OF_SHORT_ARRAY);
        customProvider.register(byte[].class, ObjectWriterAsString.OF_BYTE_ARRAY);
        customProvider.register(boolean[].class, ObjectWriterAsString.OF_BOOLEAN_ARRAY);

        asStringContext = new JSONWriter.Context(customProvider);
    }

    @Benchmark
    public void baseline_native_numbers(Blackhole bh) {
        bh.consume(JSON.toJSONString(bean, defaultContext));
    }

    @Benchmark
    public void target_as_string(Blackhole bh) {
        bh.consume(JSON.toJSONString(bean, asStringContext));
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(AsStringBenchmark.class.getSimpleName())
                .addProfiler("gc")
                .build();
        new Runner(opt).run();
    }
}

jdk21-asm(fastjson2.creator=asm):
jdk21-asm
jdk17-asm:
jdk17-asm
jdk8-asm:
jdk8-asm

Copilot AI review requested due to automatic review settings December 16, 2025 02:19
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces built-in serializers for base types to support the WriteNonStringValueAsString feature, addressing issue #3870. The implementation provides convenience for users who want to globally register serializers that write numeric and boolean values as strings.

Key Changes:

  • Adds a new ObjectWriterAsString class with pre-defined serializer instances for primitive types, wrapper classes, BigInteger, BigDecimal, and primitive arrays
  • Updates ObjectWriterCreator.getInitWriter() to recognize and use registered ObjectWriterAsString instances
  • Refines import statement in ObjectWriterCreator from wildcard to specific import

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
core/src/main/java/com/alibaba/fastjson2/writer/ObjectWriterAsString.java Introduces abstract class with 18 static serializer instances for converting base types to string representations during JSON serialization
core/src/main/java/com/alibaba/fastjson2/writer/ObjectWriterCreator.java Adds logic to detect and return ObjectWriterAsString instances in the else block of getInitWriter method; refines filter import

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}
} else {
ObjectWriter objectWriter = provider.cache.get(fieldClass);
if (objectWriter != null && objectWriter instanceof ObjectWriterAsString) {
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The instanceof check should use pattern matching or avoid redundant null check. Since this code checks if objectWriter is not null implicitly through the cache.get() call, the instanceof check will return false for null values anyway, making the explicit null check redundant. Consider simplifying to just check the instanceof condition.

Suggested change
if (objectWriter != null && objectWriter instanceof ObjectWriterAsString) {
if (objectWriter instanceof ObjectWriterAsString) {

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +12
public abstract class ObjectWriterAsString implements ObjectWriter {
private ObjectWriterAsString() {
}
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The class and its public static fields lack documentation. Since this is a public API that users will directly interact with to register custom serializers, comprehensive JavaDoc should be added to explain the purpose of this class, when to use it, and provide usage examples. Each public static field should also have documentation explaining what type it serializes.

Copilot uses AI. Check for mistakes.
Comment on lines +216 to +217
throw new JSONException("ObjectWriterAsString.write() should NOT be called directly: "
+ object.getClass().getName());
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The error message references the base class method but doesn't provide actionable guidance. Consider updating the message to explain that users should use the specific static instances (e.g., OF_INTEGER, OF_LONG) instead of calling the base write method directly, and provide an example of correct usage.

Suggested change
throw new JSONException("ObjectWriterAsString.write() should NOT be called directly: "
+ object.getClass().getName());
throw new JSONException(
"ObjectWriterAsString.write() should NOT be called directly for type: "
+ object.getClass().getName()
+ ".\n"
+ "Please use one of the specific static instances (e.g., ObjectWriterAsString.OF_INTEGER, ObjectWriterAsString.OF_LONG) instead.\n"
+ "Example: ObjectWriterAsString.OF_INTEGER.write(jsonWriter, value, fieldName, fieldType, features);"
);

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +219
public abstract class ObjectWriterAsString implements ObjectWriter {
private ObjectWriterAsString() {
}

public static final ObjectWriterAsString OF_INTEGER = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString(((Number) object).intValue());
} else {
jsonWriter.writeNull();
}
}
};
public static final ObjectWriterAsString OF_INT_VALUE = OF_INTEGER;

public static final ObjectWriterAsString OF_LONG = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString(((Number) object).longValue());
} else {
jsonWriter.writeNull();
}
}
};
public static final ObjectWriterAsString OF_LONG_VALUE = OF_LONG;

public static final ObjectWriterAsString OF_FLOAT = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString(((Number) object).floatValue());
} else {
jsonWriter.writeNull();
}
}
};
public static final ObjectWriterAsString OF_FLOAT_VALUE = OF_FLOAT;

public static final ObjectWriterAsString OF_DOUBLE = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString(((Number) object).doubleValue());
} else {
jsonWriter.writeNull();
}
}
};
public static final ObjectWriterAsString OF_DOUBLE_VALUE = OF_DOUBLE;

public static final ObjectWriterAsString OF_SHORT = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString(((Number) object).shortValue());
} else {
jsonWriter.writeNull();
}
}
};
public static final ObjectWriterAsString OF_SHORT_VALUE = OF_SHORT;

public static final ObjectWriterAsString OF_BYTE = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString(((Number) object).byteValue());
} else {
jsonWriter.writeNull();
}
}
};
public static final ObjectWriterAsString OF_BYTE_VALUE = OF_BYTE;

public static final ObjectWriterAsString OF_CHARACTER = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString(object.toString());
} else {
jsonWriter.writeNull();
}
}
};
public static final ObjectWriterAsString OF_CHAR_VALUE = OF_CHARACTER;

public static final ObjectWriterAsString OF_BOOLEAN = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString(((Boolean) object).booleanValue());
} else {
jsonWriter.writeNull();
}
}
};
public static final ObjectWriterAsString OF_BOOLEAN_VALUE = OF_BOOLEAN;

public static final ObjectWriterAsString OF_BIG_INTEGER = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeBigInt((BigInteger) object, JSONWriter.Feature.WriteNonStringValueAsString.mask);
} else {
jsonWriter.writeNull();
}
}
};

public static final ObjectWriterAsString OF_BIG_DECIMAL = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeDecimal((BigDecimal) object, JSONWriter.Feature.WriteNonStringValueAsString.mask, null);
} else {
jsonWriter.writeNull();
}
}
};

public static final ObjectWriterAsString OF_INT_ARRAY = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString((int[]) object);
} else {
jsonWriter.writeNull();
}
}
};

public static final ObjectWriterAsString OF_LONG_ARRAY = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString((long[]) object);
} else {
jsonWriter.writeNull();
}
}
};

public static final ObjectWriterAsString OF_FLOAT_ARRAY = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString((float[]) object);
} else {
jsonWriter.writeNull();
}
}
};

public static final ObjectWriterAsString OF_DOUBLE_ARRAY = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString((double[]) object);
} else {
jsonWriter.writeNull();
}
}
};

public static final ObjectWriterAsString OF_SHORT_ARRAY = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString((short[]) object);
} else {
jsonWriter.writeNull();
}
}
};

public static final ObjectWriterAsString OF_BYTE_ARRAY = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString((byte[]) object);
} else {
jsonWriter.writeNull();
}
}
};

public static final ObjectWriterAsString OF_BOOLEAN_ARRAY = new ObjectWriterAsString() {
@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object != null) {
jsonWriter.writeString((boolean[]) object);
} else {
jsonWriter.writeNull();
}
}
};

@Override
public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
if (object == null) {
jsonWriter.writeNull();
return;
}

throw new JSONException("ObjectWriterAsString.write() should NOT be called directly: "
+ object.getClass().getName());
}
}
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The new ObjectWriterAsString class lacks test coverage. Since this is a new public API that provides built-in serializers for base types, tests should be added to verify that each serializer instance correctly serializes values as strings, handles null values properly, and integrates correctly when registered with ObjectWriterProvider. Consider adding tests that cover scenarios like registering these writers globally and verifying their usage through the ObjectWriterCreator.getInitWriter method.

Copilot uses AI. Check for mistakes.
@wenshao
Copy link
Member

wenshao commented Dec 21, 2025

建议在BigDecimal/BigInteger这两个类型的Writer实现中支持WriteNonStringValueAsString,这样改动小,用户使用也更方便

@jujn
Copy link
Contributor Author

jujn commented Dec 21, 2025

建议在BigDecimal/BigInteger这两个类型的Writer实现中支持WriteNonStringValueAsString,这样改动小,用户使用也更方便

这两个序列化器目前是支持 WriteNonStringValueAsString 的:

    @Test
    public void test() {
        System.out.println(JSON.toJSONString(new Model(BigInteger.valueOf(123), BigDecimal.valueOf(456)), JSONWriter.Feature.WriteNonStringValueAsString));
        // asm、reflect模式下都会输出 {"age":"456","name":"123"}
    }

    @Data
    public static class Model {
        private BigInteger name;
        private BigDecimal age;
    }

用户意思想 ”一条配置,全局BigInteger/BigDecimal都序列化为字符串“,类似的问题#3563、#891也提到过(全局配置BooleanAsString不生效)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants