diff --git a/src/test/java/tools/jackson/databind/ser/CanonicalBigDecimalSerializer.java b/src/test/java/tools/jackson/databind/ser/CanonicalBigDecimalSerializer.java new file mode 100644 index 0000000000..e28b8b8a42 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/CanonicalBigDecimalSerializer.java @@ -0,0 +1,75 @@ +package tools.jackson.databind.ser; + +import java.math.BigDecimal; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializerProvider; +import tools.jackson.databind.ser.std.StdSerializer; + +public class CanonicalBigDecimalSerializer extends StdSerializer + implements ValueToString { + + public static final CanonicalBigDecimalSerializer INSTANCE = new CanonicalBigDecimalSerializer(); + + public static final CanonicalNumberSerializerProvider PROVIDER = new CanonicalNumberSerializerProvider() { + @Override + public StdSerializer getNumberSerializer() { + return INSTANCE; + } + + @Override + public ValueToString getValueToString() { + return INSTANCE; + } + }; + + protected CanonicalBigDecimalSerializer() { + super(BigDecimal.class); + } + + @Override + public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider provider) + throws JacksonException { + CanonicalNumberGenerator.verifyBigDecimalRange(value, provider); + + String output = convert(value); + gen.writeNumber(output); + } + + @Override + public String convert(BigDecimal value) { + // TODO Convert to exponential form if necessary + BigDecimal stripped = value.stripTrailingZeros(); + int scale = stripped.scale(); + String text = stripped.toPlainString(); + if (scale == 0) { + return text; + } + + int pos = text.indexOf('.'); + int exp; + if (pos >= 0) { + exp = pos - 1; + + if (exp == 0) { + return text; + } + + text = text.substring(0, pos) + text.substring(pos + 1); + } else { + exp = -scale; + int end = text.length(); + while (end > 0 && text.charAt(end - 1) == '0') { + end --; + } + text = text.substring(0, end); + } + + if (text.length() == 1) { + return text + 'E' + exp; + } + + return text.substring(0, 1) + '.' + text.substring(1) + 'E' + exp; + } +} \ No newline at end of file diff --git a/src/test/java/tools/jackson/databind/ser/CanonicalBigDecimalSerializerTest.java b/src/test/java/tools/jackson/databind/ser/CanonicalBigDecimalSerializerTest.java new file mode 100644 index 0000000000..380b31f453 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/CanonicalBigDecimalSerializerTest.java @@ -0,0 +1,52 @@ +package tools.jackson.databind.ser; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.Test; + +public class CanonicalBigDecimalSerializerTest { + + @Test + void testCanonicalDecimalHandling_1() throws Exception { + assertSerialized("1", new BigDecimal("1")); + } + + @Test + void testCanonicalDecimalHandling_1_000() throws Exception { + assertSerialized("1", new BigDecimal("1.000")); + } + + @Test + void testCanonicalDecimalHandling_10_1000() throws Exception { + assertSerialized("1.01E1", new BigDecimal("10.1000")); + } + + @Test + void testCanonicalDecimalHandling_1000() throws Exception { + assertSerialized("1E3", new BigDecimal("1000")); + } + + @Test + void testCanonicalDecimalHandling_0_00000000010() throws Exception { + assertSerialized("0.0000000001", new BigDecimal("0.00000000010")); + } + + @Test + void testCanonicalDecimalHandling_1000_00010() throws Exception { + assertSerialized("1.0000001E3", new BigDecimal("1000.00010")); + } + + @Test + void testCanonicalHugeDecimalHandling() throws Exception { + BigDecimal actual = new BigDecimal("123456789123456789123456789123456789.123456789123456789123456789123456789123456789000"); + assertSerialized("1.23456789123456789123456789123456789123456789123456789123456789123456789123456789E35", actual); + } + + private void assertSerialized(String expected, BigDecimal actual) { + CanonicalBigDecimalSerializer serializer = new CanonicalBigDecimalSerializer(); + assertEquals(expected, serializer.convert(actual)); + } + +} diff --git a/src/test/java/tools/jackson/databind/ser/CanonicalJsonFactory.java b/src/test/java/tools/jackson/databind/ser/CanonicalJsonFactory.java new file mode 100644 index 0000000000..250117bcb5 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/CanonicalJsonFactory.java @@ -0,0 +1,31 @@ +package tools.jackson.databind.ser; + +import java.io.Writer; +import java.math.BigDecimal; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.ObjectWriteContext; +import tools.jackson.core.io.IOContext; +import tools.jackson.core.json.JsonFactory; + +/** + * TODO Fix double numbers. This feels like a very heavy solution plus I can't + * use the JsonFactory.builder(). + */ +public class CanonicalJsonFactory extends JsonFactory { + private static final long serialVersionUID = 1L; + + private ValueToString _serializer; + + public CanonicalJsonFactory(ValueToString serializer) { + this._serializer = serializer; + } + + @Override + protected JsonGenerator _createGenerator(ObjectWriteContext writeCtxt, IOContext ioCtxt, Writer out) + throws JacksonException { + JsonGenerator delegate = super._createGenerator(writeCtxt, ioCtxt, out); + return new CanonicalNumberGenerator(delegate, _serializer); + } +} diff --git a/src/test/java/tools/jackson/databind/ser/CanonicalJsonMapper.java b/src/test/java/tools/jackson/databind/ser/CanonicalJsonMapper.java new file mode 100644 index 0000000000..1851fde2ef --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/CanonicalJsonMapper.java @@ -0,0 +1,48 @@ +package tools.jackson.databind.ser; + +import tools.jackson.core.StreamWriteFeature; +import tools.jackson.databind.MapperFeature; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; + +public class CanonicalJsonMapper { // TODO It would be great if we could extend JsonMapper but the return type of builder() is incompatible + + public static class Builder { // TODO Can't extend MapperBuilder because that needs JsonFactory as ctor arg and we only have this later + private CanonicalNumberSerializerProvider _numberSerializerProvider = CanonicalBigDecimalSerializer.PROVIDER; + private boolean _enablePrettyPrinting = false; + + private Builder() { + // Don't allow to create except via builder method + } + + public Builder prettyPrint() { + _enablePrettyPrinting = true; + _numberSerializerProvider = PrettyBigDecimalSerializer.PROVIDER; + return this; + } + + public JsonMapper build() { + CanonicalJsonFactory jsonFactory = new CanonicalJsonFactory(_numberSerializerProvider.getValueToString()); + CanonicalJsonModule module = new CanonicalJsonModule(_numberSerializerProvider.getNumberSerializer()); + + JsonMapper.Builder builder = JsonMapper.builder(jsonFactory) + .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) // + .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) // + .addModule(module); + + if (_enablePrettyPrinting) { + builder = builder // + .enable(SerializationFeature.INDENT_OUTPUT) // + .enable(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN) // + .defaultPrettyPrinter(CanonicalPrettyPrinter.INSTANCE) // + ; + } + + return builder.build(); + } + } + + public static CanonicalJsonMapper.Builder builder() { + return new Builder(); + } +} diff --git a/src/test/java/tools/jackson/databind/ser/CanonicalJsonModule.java b/src/test/java/tools/jackson/databind/ser/CanonicalJsonModule.java new file mode 100644 index 0000000000..d193e7d5c1 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/CanonicalJsonModule.java @@ -0,0 +1,18 @@ +package tools.jackson.databind.ser; + +import java.math.BigDecimal; + +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.ser.std.StdSerializer; + +public class CanonicalJsonModule extends SimpleModule { + private static final long serialVersionUID = 1L; + + public CanonicalJsonModule() { + this(CanonicalBigDecimalSerializer.INSTANCE); + } + + public CanonicalJsonModule(StdSerializer numberSerializer) { + addSerializer(BigDecimal.class, numberSerializer); + } +} diff --git a/src/test/java/tools/jackson/databind/ser/CanonicalJsonTest.java b/src/test/java/tools/jackson/databind/ser/CanonicalJsonTest.java new file mode 100644 index 0000000000..49d73a6fb5 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/CanonicalJsonTest.java @@ -0,0 +1,191 @@ +package tools.jackson.databind.ser; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; // TODO JUnit 4 or 5 for tests? + +import com.google.common.collect.Lists; + +import tools.jackson.databind.BaseTest; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.node.JsonNodeFactory; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.databind.ser.std.StdSerializer; + +class CanonicalJsonTest extends BaseTest { + + private static final double NEGATIVE_ZERO = -0.; + private static final JsonAssert JSON_ASSERT = new JsonAssert(); + private static final JsonTestResource CANONICAL_1 = new JsonTestResource("/data/canonical-1.json"); + + // TODO There are several ways to make sure we really have a negative sign. + // Double.toString(NEGATIVE_ZERO) seems to be the most simple. + @Test + void testSignOfNegativeZero() { + assertEquals("-0.0", Double.toString(Math.signum(NEGATIVE_ZERO))); + } + + @Test + void testSignOfNegativeZero2() { + long bits = Double.doubleToRawLongBits(NEGATIVE_ZERO); + assertTrue(bits < 0); + } + + @Test + void testSignOfNegativeZero3() { + long sign = 1L << (Double.SIZE - 1); // Highest bit represents the sign + long bits = Double.doubleToRawLongBits(NEGATIVE_ZERO); + assertEquals(sign, bits & sign); + } + + @Test + void testSignOfNegativeZero4() { + assertEquals("-0.0", Double.toString(NEGATIVE_ZERO)); + } + + @Test + void testNegativeZeroIsEqualToZero() { + assertEquals(0.0, NEGATIVE_ZERO, 1e-9); + } + + @Test + void testCanonicalBigDecimalSerializationTrailingZeros() throws Exception { + assertSerialized("1", new BigDecimal("1.0000"), newCanonicalMapperBuilder()); + } + + @Test + void testCanonicalNegativeZeroBigDecimal() throws Exception { + assertSerialized("0", new BigDecimal("-0"), newCanonicalMapperBuilder()); + } + + @Test + void testCanonicalNegativeZeroBigDecimal2() throws Exception { + assertSerialized("0", new BigDecimal(NEGATIVE_ZERO), newCanonicalMapperBuilder()); + } + + @Test + void testCanonicalNegativeZeroDouble() throws Exception { + assertSerialized("0", NEGATIVE_ZERO, newCanonicalMapperBuilder()); + } + + @Test + void testCanonicalDecimalHandling() throws Exception { + assertSerialized("1.01E1", new BigDecimal("10.1000"), newCanonicalMapperBuilder()); + } + + @Test + void testCanonicalHugeDecimalHandling() throws Exception { + BigDecimal actual = new BigDecimal("123456789123456789123456789123456789.123456789123456789123456789123456789123456789000"); + assertSerialized("1.23456789123456789123456789123456789123456789123456789123456789123456789123456789E35", actual, newCanonicalMapperBuilder()); + } + + @Test + void testPrettyDecimalHandling() throws Exception { + assertSerialized("10.1", new BigDecimal("10.1000"), newPrettyCanonicalMapperBuilder()); + } + + @Test + void testPrettyHugeDecimalHandling() throws Exception { + BigDecimal actual = new BigDecimal("123456789123456789123456789123456789.123456789123456789123456789123456789123456789000"); + assertSerialized("123456789123456789123456789123456789.123456789123456789123456789123456789123456789", actual, newPrettyCanonicalMapperBuilder()); + } + + @Test + void testCanonicalJsonSerialization() throws Exception { + JsonNode expected = JSON_ASSERT.loadResource(CANONICAL_1); + JsonNode actual = buildTestData(); + + assertCanonicalJson(expected, actual); + } + + @Test + void testCanonicalJsonSerializationRandomizedChildren() throws Exception { + JsonNode expected = JSON_ASSERT.loadResource(CANONICAL_1); + JsonNode actual = randomize(buildTestData()); + + assertCanonicalJson(expected, actual); + } + + @Test + void testPrettyJsonSerialization() throws Exception { + JsonNode actual = buildTestData(); + + JSON_ASSERT.assertJson(CANONICAL_1, actual); + } + + @Test + void testPrettyJsonSerializationRandomizedChildren() throws Exception { + JsonNode actual = randomize(buildTestData()); + + JSON_ASSERT.assertJson(CANONICAL_1, actual); + } + + private void assertSerialized(String expected, Object input, JsonMapper mapper) { + String actual = mapper.writeValueAsString(input); + assertEquals(expected, actual); + } + + private JsonMapper newCanonicalMapperBuilder() { + return CanonicalJsonMapper.builder().build(); + } + + private JsonMapper newPrettyCanonicalMapperBuilder() { + return CanonicalJsonMapper.builder().prettyPrint().build(); + } + + private JsonNode randomize(JsonNode input) { + if (input instanceof ObjectNode) { + List> copy = Lists.newArrayList(input.fields()); + Collections.shuffle(copy); + + Map randomized = new LinkedHashMap<>(); + copy.forEach(entry -> { + randomized.put(entry.getKey(), randomize(entry.getValue())); + }); + + return new ObjectNode(JsonNodeFactory.instance, randomized); + } else { + return input; + } + } + + private void assertCanonicalJson(JsonNode expected, JsonNode actual) { + ObjectMapper mapper = newCanonicalMapperBuilder(); + assertEquals(serialize(expected, mapper), serialize(actual, mapper)); + } + + private String serialize(JsonNode input, ObjectMapper mapper) { + // TODO Is there a better way to sort the keys than deserializing the whole tree? + Object obj = mapper.treeToValue(input, Object.class); + return mapper.writeValueAsString(obj); + } + + private JsonNode buildTestData() { + return new ObjectNode(JsonNodeFactory.instance) // + .put("-0", NEGATIVE_ZERO) // + .put("-1", -1) // + .put("0.1", new BigDecimal("0.100")) // + .put("1", new BigDecimal("1")) // + .put("10.1", new BigDecimal("10.100")) // + .put("emoji", "\uD83D\uDE03") // + .put("escape", "\u001B") // + .put("lone surrogate", "\uDEAD") // + .put("whitespace", " \t\n\r") // + ; + } + + public static class BigDecimalModule extends SimpleModule { + private static final long serialVersionUID = 1L; + + public BigDecimalModule(StdSerializer serializer) { + addSerializer(BigDecimal.class, serializer); + } + } +} diff --git a/src/test/java/tools/jackson/databind/ser/CanonicalNumberGenerator.java b/src/test/java/tools/jackson/databind/ser/CanonicalNumberGenerator.java new file mode 100644 index 0000000000..d4fb74fea3 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/CanonicalNumberGenerator.java @@ -0,0 +1,65 @@ +package tools.jackson.databind.ser; + +import java.math.BigDecimal; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.exc.StreamWriteException; +import tools.jackson.core.util.JsonGeneratorDelegate; +import tools.jackson.databind.SerializerProvider; + +public class CanonicalNumberGenerator extends JsonGeneratorDelegate { + + /** + * TODO The constant should be public or the verify method. Copied from + * `jackson-databing` class `NumberSerializer` + */ + protected final static int MAX_BIG_DECIMAL_SCALE = 9999; + + private final ValueToString _bigDecimalToString; + + public CanonicalNumberGenerator(JsonGenerator gen, ValueToString bigDecimalToString) { + super(gen); + this._bigDecimalToString = bigDecimalToString; + } + + @Override + public JsonGenerator writeNumber(double v) throws JacksonException { + BigDecimal wrapper = BigDecimal.valueOf(v); + return writeNumber(wrapper); + } + + @Override + public JsonGenerator writeNumber(BigDecimal v) throws JacksonException { + if (!verifyBigDecimalRange(v)) { + // TODO Is there a better way? I can't call delegate._reportError(). + String msg = bigDecimalOutOfRangeError(v); + throw new StreamWriteException(this, msg); + } + + String converted = _bigDecimalToString.convert(v); + return delegate.writeNumber(converted); + } + + public static boolean verifyBigDecimalRange(BigDecimal value, SerializerProvider provider) { + boolean result = verifyBigDecimalRange(value); + + if (!result) { + provider.reportMappingProblem(bigDecimalOutOfRangeError(value)); + } + + return result; + } + + public static boolean verifyBigDecimalRange(BigDecimal value) { + int scale = value.scale(); + return ((scale >= -MAX_BIG_DECIMAL_SCALE) && (scale <= MAX_BIG_DECIMAL_SCALE)); + } + + // TODO Everyone should use the same method + public static String bigDecimalOutOfRangeError(BigDecimal value) { + return String.format( + "Attempt to write plain `java.math.BigDecimal` (see StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN) with illegal scale (%d): needs to be between [-%d, %d]", + value.scale(), MAX_BIG_DECIMAL_SCALE, MAX_BIG_DECIMAL_SCALE); + } +} diff --git a/src/test/java/tools/jackson/databind/ser/CanonicalNumberSerializerProvider.java b/src/test/java/tools/jackson/databind/ser/CanonicalNumberSerializerProvider.java new file mode 100644 index 0000000000..f4cd16b8fa --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/CanonicalNumberSerializerProvider.java @@ -0,0 +1,11 @@ +package tools.jackson.databind.ser; + +import java.math.BigDecimal; + +import tools.jackson.databind.ser.std.StdSerializer; + +// TODO I need to implement an interface and ValueSerializer which is an abstract class. This seems to be the only solution. +public interface CanonicalNumberSerializerProvider { + StdSerializer getNumberSerializer(); + ValueToString getValueToString(); +} diff --git a/src/test/java/tools/jackson/databind/ser/CanonicalPrettyPrinter.java b/src/test/java/tools/jackson/databind/ser/CanonicalPrettyPrinter.java new file mode 100644 index 0000000000..d54cc4caca --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/CanonicalPrettyPrinter.java @@ -0,0 +1,23 @@ +package tools.jackson.databind.ser; + +import tools.jackson.core.PrettyPrinter; +import tools.jackson.core.util.DefaultIndenter; +import tools.jackson.core.util.DefaultPrettyPrinter; +import tools.jackson.core.util.Separators; + +public class CanonicalPrettyPrinter extends DefaultPrettyPrinter { + private static final long serialVersionUID = 1L; + private static final DefaultIndenter STABLE_INDENTEER = new DefaultIndenter(" ", "\n"); + + public static final PrettyPrinter INSTANCE = new CanonicalPrettyPrinter() + .withObjectIndenter(STABLE_INDENTEER); + + @Override + public DefaultPrettyPrinter withSeparators(Separators separators) { + _separators = separators; + // TODO it would be great if it was possible to configure this without + // overriding + _nameValueSeparatorWithSpaces = separators.getObjectNameValueSeparator() + " "; + return this; + } +} diff --git a/src/test/java/tools/jackson/databind/ser/JsonAssert.java b/src/test/java/tools/jackson/databind/ser/JsonAssert.java new file mode 100644 index 0000000000..3d34e588ca --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/JsonAssert.java @@ -0,0 +1,54 @@ +package tools.jackson.databind.ser; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.IOException; +import java.io.InputStream; + +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; + +public class JsonAssert { + + private JsonMapper _mapper; // TODO If we don't want to allow people to configure this, then everything in here can be static which would make it look a lot like JUnit Assertions. + + public JsonAssert() { + // TODO Does it make sense to use compact canonical JSON in unit tests? The diff of the comparison will be hard to use. + _mapper = CanonicalJsonMapper.builder().prettyPrint().build(); + } + + public void assertJson(JsonTestResource expected, Object actual) { + assertJson(loadResource(expected), actual); + } + + public void assertJson(JsonNode expected, Object actual) { + assertEquals(serialize(expected), serialize(actual)); + } + + public String serialize(JsonNode input) { + // TODO Is there a better way to sort the keys than deserializing the whole tree? + Object obj = _mapper.treeToValue(input, Object.class); + return _mapper.writeValueAsString(obj); + } + + public String serialize(Object data) { + if (data instanceof JsonNode) { + return serialize((JsonNode)data); + } + + // TODO Sorting mostly works except for properties defined by @JsonTypeInfo + return _mapper.writeValueAsString(data); + } + + public JsonNode loadResource(JsonTestResource resource) { + try (InputStream stream = resource.getInputStream()) { + // TODO Formatting ok? JUnit 4 or 5 here? + assertNotNull("Missing resource " + resource, stream); + + return _mapper.readTree(stream); + } catch (IOException e) { + throw new AssertionError("Error loading " + resource, e); + } + } +} diff --git a/src/test/java/tools/jackson/databind/ser/JsonTestResource.java b/src/test/java/tools/jackson/databind/ser/JsonTestResource.java new file mode 100644 index 0000000000..25c8afcfe7 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/JsonTestResource.java @@ -0,0 +1,47 @@ +package tools.jackson.databind.ser; + +import java.io.InputStream; +import java.net.URL; + +public class JsonTestResource { + + private String resourceName; + private ClassLoader classLoader; + + public JsonTestResource(String resourceName) { + this(resourceName, JsonTestResource.class.getClassLoader()); + } + + public JsonTestResource(String resourceName, ClassLoader classLoader) { + this.resourceName = fix(resourceName); + this.classLoader = classLoader; + } + + private static String fix(String resourceName) { + if (resourceName.startsWith("/")) { + return resourceName.substring(1); + } + return resourceName; + } + + public URL getURL() { + return classLoader.getResource(resourceName); + } + + public InputStream getInputStream() { + return classLoader.getResourceAsStream(resourceName); + } + + @Override + public String toString() { + URL url = getURL(); + String content; + if (url == null) { + content = "resourceName=" + resourceName + " (not on classpath)"; + } else { + content = "url=" + url; + } + + return getClass().getSimpleName() + "(" + content + ")"; + } +} diff --git a/src/test/java/tools/jackson/databind/ser/PrettyBigDecimalSerializer.java b/src/test/java/tools/jackson/databind/ser/PrettyBigDecimalSerializer.java new file mode 100644 index 0000000000..14cf318a53 --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/PrettyBigDecimalSerializer.java @@ -0,0 +1,44 @@ +package tools.jackson.databind.ser; + +import java.math.BigDecimal; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializerProvider; +import tools.jackson.databind.ser.std.StdSerializer; + +public class PrettyBigDecimalSerializer extends StdSerializer + implements ValueToString { + + public static final PrettyBigDecimalSerializer INSTANCE = new PrettyBigDecimalSerializer(); + + public static final CanonicalNumberSerializerProvider PROVIDER = new CanonicalNumberSerializerProvider() { + @Override + public StdSerializer getNumberSerializer() { + return INSTANCE; + } + + @Override + public ValueToString getValueToString() { + return INSTANCE; + } + }; + + protected PrettyBigDecimalSerializer() { + super(BigDecimal.class); + } + + @Override + public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider provider) + throws JacksonException { + CanonicalNumberGenerator.verifyBigDecimalRange(value, provider); + + String output = convert(value); + gen.writeNumber(output); + } + + @Override + public String convert(BigDecimal value) { + return value.stripTrailingZeros().toPlainString(); + } +} \ No newline at end of file diff --git a/src/test/java/tools/jackson/databind/ser/ValueToString.java b/src/test/java/tools/jackson/databind/ser/ValueToString.java new file mode 100644 index 0000000000..939ab409da --- /dev/null +++ b/src/test/java/tools/jackson/databind/ser/ValueToString.java @@ -0,0 +1,5 @@ +package tools.jackson.databind.ser; + +public interface ValueToString { + String convert(T value); +} diff --git a/src/test/resources/data/canonical-1.json b/src/test/resources/data/canonical-1.json new file mode 100644 index 0000000000..a9b62c644a --- /dev/null +++ b/src/test/resources/data/canonical-1.json @@ -0,0 +1 @@ +{"-0":0,"-1":-1,"0.1":1.0E-1,"1":1,"10.1":1.01E1,"emoji":"😃","escape":"\u001B","lone surrogate":"\uDEAD","whitespace":" \t\n\r"} \ No newline at end of file