From 7b9cf6af3207ff2cdcd2f1bb212278d4d2bc471c Mon Sep 17 00:00:00 2001 From: Aaron Digulla Date: Tue, 6 Jun 2023 22:07:04 +0200 Subject: [PATCH 1/9] Support for canonical JSON output in Jackson. I kept all classes in the tests folder since there will be many more changes and it's easier when everything is in a single place. --- .../ser/CanonicalBigDecimalSerializer.java | 62 +++++ .../CanonicalBigDecimalSerializerTest.java | 52 ++++ .../databind/ser/CanonicalJsonFactory.java | 30 ++ .../databind/ser/CanonicalJsonTest.java | 262 ++++++++++++++++++ .../ser/CanonicalNumberGenerator.java | 66 +++++ .../databind/ser/CanonicalPrettyPrinter.java | 23 ++ .../jackson/databind/ser/ValueToString.java | 5 + src/test/resources/data/canonical-1.json | 1 + 8 files changed, 501 insertions(+) create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializer.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializerTest.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonFactory.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberGenerator.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalPrettyPrinter.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/ValueToString.java create mode 100644 src/test/resources/data/canonical-1.json diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializer.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializer.java new file mode 100644 index 0000000000..1b2d22b6de --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializer.java @@ -0,0 +1,62 @@ +package com.fasterxml.jackson.databind.ser; + +import java.io.IOException; +import java.math.BigDecimal; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +public class CanonicalBigDecimalSerializer extends StdSerializer + implements ValueToString { + private static final long serialVersionUID = 1L; + + protected CanonicalBigDecimalSerializer() { + super(BigDecimal.class); + } + + @Override + public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider provider) + throws IOException { + 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/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializerTest.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializerTest.java new file mode 100644 index 0000000000..d49e51acfd --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializerTest.java @@ -0,0 +1,52 @@ +package com.fasterxml.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/com/fasterxml/jackson/databind/ser/CanonicalJsonFactory.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonFactory.java new file mode 100644 index 0000000000..e7736248bd --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonFactory.java @@ -0,0 +1,30 @@ +package com.fasterxml.jackson.databind.ser; + +import java.io.IOException; +import java.io.Writer; +import java.math.BigDecimal; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.io.IOContext; +import com.fasterxml.jackson.core.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(Writer out, IOContext ioCtxt) + throws IOException { + JsonGenerator delegate = super._createGenerator(out, ioCtxt); + return new CanonicalNumberGenerator(delegate, _serializer); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java new file mode 100644 index 0000000000..c74d4a19b6 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java @@ -0,0 +1,262 @@ +package com.fasterxml.jackson.databind.ser; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +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 com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.StreamWriteFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.json.JsonMapper.Builder; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +class CanonicalJsonTest { + + private static final ObjectMapper MAPPER = JsonMapper.builder().build(); + private static final double NEGATIVE_ZERO = -0.; + + // 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 = loadData("canonical-1.json"); + JsonNode actual = buildTestData(); + + assertCanonicalJson(expected, actual); + } + + @Test + void testCanonicalJsonSerializationRandomizedChildren() throws Exception { + JsonNode expected = loadData("canonical-1.json"); + JsonNode actual = randomize(buildTestData()); + + assertCanonicalJson(expected, actual); + } + + @Test + void testPrettyJsonSerialization() throws Exception { + JsonNode expected = loadData("canonical-1.json"); + JsonNode actual = buildTestData(); + + assertPrettyJson(expected, actual); + } + + @Test + void testPrettyJsonSerializationRandomizedChildren() throws Exception { + JsonNode expected = loadData("canonical-1.json"); + JsonNode actual = randomize(buildTestData()); + + assertPrettyJson(expected, actual); + } + + private void assertSerialized(String expected, Object input, JsonMapper.Builder builder) throws JsonProcessingException { + ObjectMapper mapper = builder.build(); + + String actual = mapper.writeValueAsString(input); + assertEquals(expected, actual); + } + + private Builder newCanonicalMapperBuilder() { + CanonicalBigDecimalSerializer serializer = new CanonicalBigDecimalSerializer(); + SimpleModule bigDecimalModule = new BigDecimalModule(serializer); + + JsonFactory factory = new CanonicalJsonFactory(serializer); + return sharedConfig(JsonMapper.builder(factory)) + .addModules(bigDecimalModule); + } + + private Builder newPrettyCanonicalMapperBuilder() { + PrettyBigDecimalSerializer serializer = new PrettyBigDecimalSerializer(); + SimpleModule bigDecimalModule = new BigDecimalModule(serializer); + + JsonFactory factory = new CanonicalJsonFactory(serializer); + return sharedConfig(JsonMapper.builder(factory)) // + .enable(SerializationFeature.INDENT_OUTPUT) // + .enable(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN) // + .defaultPrettyPrinter(CanonicalPrettyPrinter.INSTANCE) // + .addModules(bigDecimalModule); + } + + private JsonMapper.Builder sharedConfig(JsonMapper.Builder builder) { + return builder.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) + .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY); + } + + 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) throws JsonProcessingException, IllegalArgumentException { + ObjectMapper mapper = newCanonicalMapperBuilder().build(); + assertEquals(serialize(expected, mapper), serialize(actual, mapper)); + } + + private void assertPrettyJson(JsonNode expected, JsonNode actual) throws JsonProcessingException, IllegalArgumentException { + ObjectMapper mapper = newPrettyCanonicalMapperBuilder().build(); + assertEquals(serialize(expected, mapper), serialize(actual, mapper)); + } + + private String serialize(JsonNode input, ObjectMapper mapper) throws JsonProcessingException, IllegalArgumentException { + // 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 loadData(String fileName) throws IOException { + String resource = "/data/" + fileName; + try (InputStream stream = getClass().getResourceAsStream(resource)) { + // TODO Formatting ok? JUnit 4 or 5 here? + assertNotNull("Missing resource " + resource, stream); + + return MAPPER.readTree(stream); + } + } + + 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 PrettyBigDecimalSerializer extends StdSerializer + implements ValueToString { + private static final long serialVersionUID = 1L; + + protected PrettyBigDecimalSerializer() { + super(BigDecimal.class); + } + + @Override + public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider provider) + throws IOException { + CanonicalNumberGenerator.verifyBigDecimalRange(value, provider); + + String output = convert(value); + gen.writeNumber(output); + } + + @Override + public String convert(BigDecimal value) { + return value.stripTrailingZeros().toPlainString(); + } + } + + 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/com/fasterxml/jackson/databind/ser/CanonicalNumberGenerator.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberGenerator.java new file mode 100644 index 0000000000..ad08f300a8 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberGenerator.java @@ -0,0 +1,66 @@ +package com.fasterxml.jackson.databind.ser; + +import java.io.IOException; +import java.math.BigDecimal; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.util.JsonGeneratorDelegate; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.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 void writeNumber(double v) throws IOException { + BigDecimal wrapper = BigDecimal.valueOf(v); + writeNumber(wrapper); + } + + @Override + public void writeNumber(BigDecimal v) throws IOException { + if (!verifyBigDecimalRange(v)) { + // TODO Is there a better way? I can't call delegate._reportError(). + String msg = bigDecimalOutOfRangeError(v); + throw new JsonGenerationException(msg, this); + } + + String converted = _bigDecimalToString.convert(v); + delegate.writeNumber(converted); + } + + public static boolean verifyBigDecimalRange(BigDecimal value, SerializerProvider provider) throws JsonMappingException { + 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/com/fasterxml/jackson/databind/ser/CanonicalPrettyPrinter.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalPrettyPrinter.java new file mode 100644 index 0000000000..ee3b8066f7 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalPrettyPrinter.java @@ -0,0 +1,23 @@ +package com.fasterxml.jackson.databind.ser; + +import com.fasterxml.jackson.core.PrettyPrinter; +import com.fasterxml.jackson.core.util.DefaultIndenter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.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 + _objectFieldValueSeparatorWithSpaces = separators.getObjectFieldValueSeparator() + " "; + return this; + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/ValueToString.java b/src/test/java/com/fasterxml/jackson/databind/ser/ValueToString.java new file mode 100644 index 0000000000..0349427835 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/ValueToString.java @@ -0,0 +1,5 @@ +package com.fasterxml.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 From e5ebecbcbd58ee0510b0975e28680ce83a12a76b Mon Sep 17 00:00:00 2001 From: Aaron Digulla Date: Tue, 6 Jun 2023 22:26:48 +0200 Subject: [PATCH 2/9] Extracted setup code into CanonicalJsonMapper plus a few helper classes. Added JsonAssert with several assertion methods that can be used similar to assertEquals(). --- .../ser/CanonicalBigDecimalSerializer.java | 14 ++ .../databind/ser/CanonicalJsonMapper.java | 48 +++++++ .../databind/ser/CanonicalJsonModule.java | 18 +++ .../databind/ser/CanonicalJsonTest.java | 125 ++++-------------- .../CanonicalNumberSerializerProvider.java | 11 ++ .../jackson/databind/ser/JsonAssert.java | 63 +++++++++ .../databind/ser/JsonTestResource.java | 47 +++++++ .../ser/PrettyBigDecimalSerializer.java | 46 +++++++ 8 files changed, 275 insertions(+), 97 deletions(-) create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonModule.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberSerializerProvider.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/JsonTestResource.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/PrettyBigDecimalSerializer.java diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializer.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializer.java index 1b2d22b6de..447b0d221b 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializer.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializer.java @@ -11,6 +11,20 @@ public class CanonicalBigDecimalSerializer extends StdSerializer implements ValueToString { private static final long serialVersionUID = 1L; + 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); } diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java new file mode 100644 index 0000000000..81b8c64a90 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java @@ -0,0 +1,48 @@ +package com.fasterxml.jackson.databind.ser; + +import com.fasterxml.jackson.core.StreamWriteFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.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/com/fasterxml/jackson/databind/ser/CanonicalJsonModule.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonModule.java new file mode 100644 index 0000000000..807c9e0d71 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonModule.java @@ -0,0 +1,18 @@ +package com.fasterxml.jackson.databind.ser; + +import java.math.BigDecimal; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.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/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java index c74d4a19b6..0f3f992742 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java @@ -1,11 +1,8 @@ package com.fasterxml.jackson.databind.ser; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import java.io.IOException; -import java.io.InputStream; import java.math.BigDecimal; import java.util.Collections; import java.util.LinkedHashMap; @@ -14,27 +11,20 @@ import org.junit.jupiter.api.Test; // TODO JUnit 4 or 5 for tests? -import com.google.common.collect.Lists; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.StreamWriteFeature; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.databind.json.JsonMapper.Builder; -import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.google.common.collect.Lists; class CanonicalJsonTest { - private static final ObjectMapper MAPPER = JsonMapper.builder().build(); 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. @@ -110,7 +100,7 @@ void testPrettyHugeDecimalHandling() throws Exception { @Test void testCanonicalJsonSerialization() throws Exception { - JsonNode expected = loadData("canonical-1.json"); + JsonNode expected = JSON_ASSERT.loadResource(CANONICAL_1); JsonNode actual = buildTestData(); assertCanonicalJson(expected, actual); @@ -118,7 +108,7 @@ void testCanonicalJsonSerialization() throws Exception { @Test void testCanonicalJsonSerializationRandomizedChildren() throws Exception { - JsonNode expected = loadData("canonical-1.json"); + JsonNode expected = JSON_ASSERT.loadResource(CANONICAL_1); JsonNode actual = randomize(buildTestData()); assertCanonicalJson(expected, actual); @@ -126,51 +116,34 @@ void testCanonicalJsonSerializationRandomizedChildren() throws Exception { @Test void testPrettyJsonSerialization() throws Exception { - JsonNode expected = loadData("canonical-1.json"); JsonNode actual = buildTestData(); - assertPrettyJson(expected, actual); + JSON_ASSERT.assertJson(CANONICAL_1, actual); } @Test void testPrettyJsonSerializationRandomizedChildren() throws Exception { - JsonNode expected = loadData("canonical-1.json"); JsonNode actual = randomize(buildTestData()); - assertPrettyJson(expected, actual); + JSON_ASSERT.assertJson(CANONICAL_1, actual); } - private void assertSerialized(String expected, Object input, JsonMapper.Builder builder) throws JsonProcessingException { - ObjectMapper mapper = builder.build(); - - String actual = mapper.writeValueAsString(input); + private void assertSerialized(String expected, Object input, JsonMapper mapper) { + String actual; + try { + actual = mapper.writeValueAsString(input); + } catch (JsonProcessingException e) { + throw new AssertionError("Unable to serialize " + input, e); + } assertEquals(expected, actual); } - private Builder newCanonicalMapperBuilder() { - CanonicalBigDecimalSerializer serializer = new CanonicalBigDecimalSerializer(); - SimpleModule bigDecimalModule = new BigDecimalModule(serializer); - - JsonFactory factory = new CanonicalJsonFactory(serializer); - return sharedConfig(JsonMapper.builder(factory)) - .addModules(bigDecimalModule); - } - - private Builder newPrettyCanonicalMapperBuilder() { - PrettyBigDecimalSerializer serializer = new PrettyBigDecimalSerializer(); - SimpleModule bigDecimalModule = new BigDecimalModule(serializer); - - JsonFactory factory = new CanonicalJsonFactory(serializer); - return sharedConfig(JsonMapper.builder(factory)) // - .enable(SerializationFeature.INDENT_OUTPUT) // - .enable(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN) // - .defaultPrettyPrinter(CanonicalPrettyPrinter.INSTANCE) // - .addModules(bigDecimalModule); + private JsonMapper newCanonicalMapperBuilder() { + return CanonicalJsonMapper.builder().build(); } - private JsonMapper.Builder sharedConfig(JsonMapper.Builder builder) { - return builder.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) - .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY); + private JsonMapper newPrettyCanonicalMapperBuilder() { + return CanonicalJsonMapper.builder().prettyPrint().build(); } private JsonNode randomize(JsonNode input) { @@ -189,29 +162,18 @@ private JsonNode randomize(JsonNode input) { } } - private void assertCanonicalJson(JsonNode expected, JsonNode actual) throws JsonProcessingException, IllegalArgumentException { - ObjectMapper mapper = newCanonicalMapperBuilder().build(); - assertEquals(serialize(expected, mapper), serialize(actual, mapper)); - } - - private void assertPrettyJson(JsonNode expected, JsonNode actual) throws JsonProcessingException, IllegalArgumentException { - ObjectMapper mapper = newPrettyCanonicalMapperBuilder().build(); + private void assertCanonicalJson(JsonNode expected, JsonNode actual) { + ObjectMapper mapper = newCanonicalMapperBuilder(); assertEquals(serialize(expected, mapper), serialize(actual, mapper)); } - private String serialize(JsonNode input, ObjectMapper mapper) throws JsonProcessingException, IllegalArgumentException { - // 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 loadData(String fileName) throws IOException { - String resource = "/data/" + fileName; - try (InputStream stream = getClass().getResourceAsStream(resource)) { - // TODO Formatting ok? JUnit 4 or 5 here? - assertNotNull("Missing resource " + resource, stream); - - return MAPPER.readTree(stream); + private String serialize(JsonNode input, ObjectMapper mapper) { + try { + // 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); + } catch (JacksonException e) { + throw new AssertionError("Unable to serialize " + input, e); } } @@ -228,35 +190,4 @@ private JsonNode buildTestData() { .put("whitespace", " \t\n\r") // ; } - - public static class PrettyBigDecimalSerializer extends StdSerializer - implements ValueToString { - private static final long serialVersionUID = 1L; - - protected PrettyBigDecimalSerializer() { - super(BigDecimal.class); - } - - @Override - public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider provider) - throws IOException { - CanonicalNumberGenerator.verifyBigDecimalRange(value, provider); - - String output = convert(value); - gen.writeNumber(output); - } - - @Override - public String convert(BigDecimal value) { - return value.stripTrailingZeros().toPlainString(); - } - } - - 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/com/fasterxml/jackson/databind/ser/CanonicalNumberSerializerProvider.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberSerializerProvider.java new file mode 100644 index 0000000000..281371473e --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberSerializerProvider.java @@ -0,0 +1,11 @@ +package com.fasterxml.jackson.databind.ser; + +import java.math.BigDecimal; + +import com.fasterxml.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/com/fasterxml/jackson/databind/ser/JsonAssert.java b/src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java new file mode 100644 index 0000000000..be2f82accb --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java @@ -0,0 +1,63 @@ +package com.fasterxml.jackson.databind.ser; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.IOException; +import java.io.InputStream; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.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) { + try { + // 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); + } catch(JacksonException e) { + throw new AssertionError("Serializing failed: " + input, e); + } + } + + public String serialize(Object data) { + if (data instanceof JsonNode) { + return serialize((JsonNode)data); + } + + // TODO Sorting mostly works except for properties defined by @JsonTypeInfo + try { + return _mapper.writeValueAsString(data); + } catch(JacksonException e) { + throw new AssertionError("Serializing failed: " + data, e); + } + } + + 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/com/fasterxml/jackson/databind/ser/JsonTestResource.java b/src/test/java/com/fasterxml/jackson/databind/ser/JsonTestResource.java new file mode 100644 index 0000000000..76b3eed84e --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/JsonTestResource.java @@ -0,0 +1,47 @@ +package com.fasterxml.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/com/fasterxml/jackson/databind/ser/PrettyBigDecimalSerializer.java b/src/test/java/com/fasterxml/jackson/databind/ser/PrettyBigDecimalSerializer.java new file mode 100644 index 0000000000..4ef2cee044 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/PrettyBigDecimalSerializer.java @@ -0,0 +1,46 @@ +package com.fasterxml.jackson.databind.ser; + +import java.io.IOException; +import java.math.BigDecimal; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +public class PrettyBigDecimalSerializer extends StdSerializer + implements ValueToString { + + private static final long serialVersionUID = 1L; + + 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 IOException { + 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 From c89e22f61325c10dc2ba9d8be0285214212f4706 Mon Sep 17 00:00:00 2001 From: Aaron Digulla Date: Wed, 14 Jun 2023 22:20:23 +0200 Subject: [PATCH 3/9] Use JsonGenerator decorator to avoid extending JsonFactory. This way, we can use the official JsonFactory builder. --- .../databind/ser/CanonicalJsonFactory.java | 30 ------------------- .../ser/CanonicalJsonGeneratorDecorator.java | 21 +++++++++++++ .../databind/ser/CanonicalJsonMapper.java | 9 ++++-- 3 files changed, 28 insertions(+), 32 deletions(-) delete mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonFactory.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonGeneratorDecorator.java diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonFactory.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonFactory.java deleted file mode 100644 index e7736248bd..0000000000 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonFactory.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.fasterxml.jackson.databind.ser; - -import java.io.IOException; -import java.io.Writer; -import java.math.BigDecimal; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.io.IOContext; -import com.fasterxml.jackson.core.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(Writer out, IOContext ioCtxt) - throws IOException { - JsonGenerator delegate = super._createGenerator(out, ioCtxt); - return new CanonicalNumberGenerator(delegate, _serializer); - } -} diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonGeneratorDecorator.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonGeneratorDecorator.java new file mode 100644 index 0000000000..4e670a1f7b --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonGeneratorDecorator.java @@ -0,0 +1,21 @@ +package com.fasterxml.jackson.databind.ser; + +import java.math.BigDecimal; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonGeneratorDecorator; + +public class CanonicalJsonGeneratorDecorator implements JsonGeneratorDecorator { + + private ValueToString _serializer; + + public CanonicalJsonGeneratorDecorator(ValueToString serializer) { + this._serializer = serializer; + } + + @Override + public JsonGenerator decorate(JsonFactory factory, JsonGenerator generator) { + return new CanonicalNumberGenerator(generator, _serializer); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java index 81b8c64a90..ab48f5708c 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java @@ -1,5 +1,7 @@ package com.fasterxml.jackson.databind.ser; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGeneratorDecorator; import com.fasterxml.jackson.core.StreamWriteFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.SerializationFeature; @@ -22,10 +24,13 @@ public Builder prettyPrint() { } public JsonMapper build() { - CanonicalJsonFactory jsonFactory = new CanonicalJsonFactory(_numberSerializerProvider.getValueToString()); + JsonGeneratorDecorator decorator = new CanonicalJsonGeneratorDecorator(_numberSerializerProvider.getValueToString()); CanonicalJsonModule module = new CanonicalJsonModule(_numberSerializerProvider.getNumberSerializer()); - JsonMapper.Builder builder = JsonMapper.builder(jsonFactory) + JsonFactory factory = JsonFactory.builder() // + .decorateWith(decorator) + .build(); + JsonMapper.Builder builder = JsonMapper.builder(factory) // .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) // .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) // .addModule(module); From 62882ccc5975642968ee8c44cc56ef6ab837de4d Mon Sep 17 00:00:00 2001 From: Aaron Digulla Date: Wed, 14 Jun 2023 22:27:57 +0200 Subject: [PATCH 4/9] Code cleanup --- .../jackson/databind/ser/CanonicalJsonTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java index 0f3f992742..913b98d78d 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java @@ -22,6 +22,8 @@ class CanonicalJsonTest { + private static final BigDecimal TEN_POINT_1_WITH_TRAILING_ZEROES = new BigDecimal("10.1000"); + private static final BigDecimal VERY_BIG_DECIMAL = new BigDecimal("123456789123456789123456789123456789.123456789123456789123456789123456789123456789000"); 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"); @@ -78,24 +80,22 @@ void testCanonicalNegativeZeroDouble() throws Exception { @Test void testCanonicalDecimalHandling() throws Exception { - assertSerialized("1.01E1", new BigDecimal("10.1000"), newCanonicalMapperBuilder()); + assertSerialized("1.01E1", TEN_POINT_1_WITH_TRAILING_ZEROES, newCanonicalMapperBuilder()); } @Test void testCanonicalHugeDecimalHandling() throws Exception { - BigDecimal actual = new BigDecimal("123456789123456789123456789123456789.123456789123456789123456789123456789123456789000"); - assertSerialized("1.23456789123456789123456789123456789123456789123456789123456789123456789123456789E35", actual, newCanonicalMapperBuilder()); + assertSerialized("1.23456789123456789123456789123456789123456789123456789123456789123456789123456789E35", VERY_BIG_DECIMAL, newCanonicalMapperBuilder()); } @Test void testPrettyDecimalHandling() throws Exception { - assertSerialized("10.1", new BigDecimal("10.1000"), newPrettyCanonicalMapperBuilder()); + assertSerialized("10.1", TEN_POINT_1_WITH_TRAILING_ZEROES, newPrettyCanonicalMapperBuilder()); } @Test void testPrettyHugeDecimalHandling() throws Exception { - BigDecimal actual = new BigDecimal("123456789123456789123456789123456789.123456789123456789123456789123456789123456789000"); - assertSerialized("123456789123456789123456789123456789.123456789123456789123456789123456789123456789", actual, newPrettyCanonicalMapperBuilder()); + assertSerialized("123456789123456789123456789123456789.123456789123456789123456789123456789123456789", VERY_BIG_DECIMAL, newPrettyCanonicalMapperBuilder()); } @Test @@ -183,7 +183,7 @@ private JsonNode buildTestData() { .put("-1", -1) // .put("0.1", new BigDecimal("0.100")) // .put("1", new BigDecimal("1")) // - .put("10.1", new BigDecimal("10.100")) // + .put("10.1", TEN_POINT_1_WITH_TRAILING_ZEROES) // .put("emoji", "\uD83D\uDE03") // .put("escape", "\u001B") // .put("lone surrogate", "\uDEAD") // From f2881dfa614e125993ccefc2969385aa2fd184a5 Mon Sep 17 00:00:00 2001 From: Aaron Digulla Date: Mon, 19 Jun 2023 22:05:19 +0200 Subject: [PATCH 5/9] Replaced custom pretty printer with one that uses the new spacing API. Replaced hack to sort properties with JsonNodeFeature.WRITE_PROPERTIES_SORTED. Deleted several TODOs that are obsolete. --- .../databind/ser/CanonicalJsonMapper.java | 18 +++++++-- .../databind/ser/CanonicalJsonTest.java | 40 +++++-------------- .../ser/CanonicalNumberGenerator.java | 1 - .../CanonicalNumberSerializerProvider.java | 1 - .../databind/ser/CanonicalPrettyPrinter.java | 23 ----------- .../jackson/databind/ser/JsonAssert.java | 25 +++++++----- 6 files changed, 38 insertions(+), 70 deletions(-) delete mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalPrettyPrinter.java diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java index ab48f5708c..d9e72c9d92 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java @@ -2,14 +2,25 @@ import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGeneratorDecorator; +import com.fasterxml.jackson.core.PrettyPrinter; import com.fasterxml.jackson.core.StreamWriteFeature; +import com.fasterxml.jackson.core.util.DefaultIndenter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.core.util.Separators; +import com.fasterxml.jackson.core.util.Separators.Spacing; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.cfg.JsonNodeFeature; import com.fasterxml.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 class CanonicalJsonMapper { + public static final DefaultIndenter CANONICAL_INDENTEER = new DefaultIndenter(" ", "\n"); - public static class Builder { // TODO Can't extend MapperBuilder because that needs JsonFactory as ctor arg and we only have this later + public static final PrettyPrinter CANONICAL_PRETTY_PRINTER = new DefaultPrettyPrinter() + .withObjectIndenter(CANONICAL_INDENTEER) + .withSeparators(Separators.createDefaultInstance().withObjectFieldValueSpacing(Spacing.AFTER)); + + public static class Builder { private CanonicalNumberSerializerProvider _numberSerializerProvider = CanonicalBigDecimalSerializer.PROVIDER; private boolean _enablePrettyPrinting = false; @@ -33,13 +44,14 @@ public JsonMapper build() { JsonMapper.Builder builder = JsonMapper.builder(factory) // .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) // .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) // + .enable(JsonNodeFeature.WRITE_PROPERTIES_SORTED) // .addModule(module); if (_enablePrettyPrinting) { builder = builder // .enable(SerializationFeature.INDENT_OUTPUT) // .enable(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN) // - .defaultPrettyPrinter(CanonicalPrettyPrinter.INSTANCE) // + .defaultPrettyPrinter(CANONICAL_PRETTY_PRINTER) // ; } diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java index 913b98d78d..855fd55a60 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java @@ -1,7 +1,6 @@ package com.fasterxml.jackson.databind.ser; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import java.math.BigDecimal; import java.util.Collections; @@ -9,7 +8,7 @@ import java.util.List; import java.util.Map; -import org.junit.jupiter.api.Test; // TODO JUnit 4 or 5 for tests? +import org.junit.jupiter.api.Test; import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonProcessingException; @@ -28,28 +27,8 @@ class CanonicalJsonTest { 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)); } @@ -72,6 +51,11 @@ void testCanonicalNegativeZeroBigDecimal() throws Exception { void testCanonicalNegativeZeroBigDecimal2() throws Exception { assertSerialized("0", new BigDecimal(NEGATIVE_ZERO), newCanonicalMapperBuilder()); } + + @Test + void testCanonicalNegativeZeroBigDecimal3() throws Exception { + assertSerialized("0", BigDecimal.valueOf(NEGATIVE_ZERO), newCanonicalMapperBuilder()); + } @Test void testCanonicalNegativeZeroDouble() throws Exception { @@ -90,12 +74,12 @@ void testCanonicalHugeDecimalHandling() throws Exception { @Test void testPrettyDecimalHandling() throws Exception { - assertSerialized("10.1", TEN_POINT_1_WITH_TRAILING_ZEROES, newPrettyCanonicalMapperBuilder()); + JSON_ASSERT.assertSerialized("10.1", TEN_POINT_1_WITH_TRAILING_ZEROES); } @Test void testPrettyHugeDecimalHandling() throws Exception { - assertSerialized("123456789123456789123456789123456789.123456789123456789123456789123456789123456789", VERY_BIG_DECIMAL, newPrettyCanonicalMapperBuilder()); + JSON_ASSERT.assertSerialized("123456789123456789123456789123456789.123456789123456789123456789123456789123456789", VERY_BIG_DECIMAL); } @Test @@ -141,10 +125,6 @@ private void assertSerialized(String expected, Object input, JsonMapper mapper) private JsonMapper newCanonicalMapperBuilder() { return CanonicalJsonMapper.builder().build(); } - - private JsonMapper newPrettyCanonicalMapperBuilder() { - return CanonicalJsonMapper.builder().prettyPrint().build(); - } private JsonNode randomize(JsonNode input) { if (input instanceof ObjectNode) { @@ -169,9 +149,7 @@ private void assertCanonicalJson(JsonNode expected, JsonNode actual) { private String serialize(JsonNode input, ObjectMapper mapper) { try { - // 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); + return mapper.writeValueAsString(input); } catch (JacksonException e) { throw new AssertionError("Unable to serialize " + input, e); } diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberGenerator.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberGenerator.java index ad08f300a8..a13193c591 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberGenerator.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberGenerator.java @@ -33,7 +33,6 @@ public void writeNumber(double v) throws IOException { @Override public void writeNumber(BigDecimal v) throws IOException { if (!verifyBigDecimalRange(v)) { - // TODO Is there a better way? I can't call delegate._reportError(). String msg = bigDecimalOutOfRangeError(v); throw new JsonGenerationException(msg, this); } diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberSerializerProvider.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberSerializerProvider.java index 281371473e..814e811a62 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberSerializerProvider.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberSerializerProvider.java @@ -4,7 +4,6 @@ import com.fasterxml.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/com/fasterxml/jackson/databind/ser/CanonicalPrettyPrinter.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalPrettyPrinter.java deleted file mode 100644 index ee3b8066f7..0000000000 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalPrettyPrinter.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.fasterxml.jackson.databind.ser; - -import com.fasterxml.jackson.core.PrettyPrinter; -import com.fasterxml.jackson.core.util.DefaultIndenter; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; -import com.fasterxml.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 - _objectFieldValueSeparatorWithSpaces = separators.getObjectFieldValueSeparator() + " "; - return this; - } -} diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java b/src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java index be2f82accb..cec9f304ff 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java @@ -15,33 +15,36 @@ 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(); + _mapper = CanonicalJsonMapper.builder() // + .prettyPrint() // Always pretty print to make the diff in failed tests easier to use + .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 void assertSerialized(String expectedJson, Object actual) { + assertEquals(expectedJson, serialize(actual)); + } + public String serialize(JsonNode input) { try { - // 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); + return _mapper.writeValueAsString(input); } catch(JacksonException e) { throw new AssertionError("Serializing failed: " + input, e); } } - + public String serialize(Object data) { if (data instanceof JsonNode) { return serialize((JsonNode)data); } - + // TODO Sorting mostly works except for properties defined by @JsonTypeInfo try { return _mapper.writeValueAsString(data); @@ -49,7 +52,7 @@ public String serialize(Object data) { throw new AssertionError("Serializing failed: " + data, e); } } - + public JsonNode loadResource(JsonTestResource resource) { try (InputStream stream = resource.getInputStream()) { // TODO Formatting ok? JUnit 4 or 5 here? From 26b881cf1b740f71c4005309426eff55d5041efd Mon Sep 17 00:00:00 2001 From: Aaron Digulla Date: Mon, 19 Jun 2023 22:23:09 +0200 Subject: [PATCH 6/9] Documentation. Added tests for @JsonTypeInfo. Here, the sorting is still broken. --- .../databind/ser/CanonicalJsonTest.java | 79 +++++++++++++++++++ .../jackson/databind/ser/JsonAssert.java | 14 ++++ 2 files changed, 93 insertions(+) diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java index 855fd55a60..b258a0604b 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java @@ -10,6 +10,8 @@ import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -111,6 +113,36 @@ void testPrettyJsonSerializationRandomizedChildren() throws Exception { JSON_ASSERT.assertJson(CANONICAL_1, actual); } + + @Test + void testJsonTypeInfoSorting() throws Exception { + Impl1 inst = new Impl1(); + inst.setValue(97); + + JSON_ASSERT.assertStableSerialization( + "{\n" + + " \"type\": \"i1\",\n" // TODO this property should be after name + + " \"name\": \"Impl1\",\n" + + " \"value\": 97\n" + + "}", + inst, + TypeBase.class); + } + + @Test + void testJsonTypeInfoSorting2() throws Exception { + Impl2 inst = new Impl2(); + inst.setDecimal(3.1415); + + JSON_ASSERT.assertStableSerialization( + "{\n" + + " \"type\": \"i2\",\n" // TODO this property should be after name + + " \"decimal\": 3.1415,\n" + + " \"name\": \"Impl2\"\n" + + "}", + inst, + TypeBase.class); + } private void assertSerialized(String expected, Object input, JsonMapper mapper) { String actual; @@ -168,4 +200,51 @@ private JsonNode buildTestData() { .put("whitespace", " \t\n\r") // ; } + + @JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="type") + @JsonSubTypes({ + @JsonSubTypes.Type(value=Impl1.class, name="i1"), + @JsonSubTypes.Type(value=Impl2.class, name="i2") + }) + public interface TypeBase { + String getName(); + } + + public static class Impl1 implements TypeBase { + private String name = "Impl1"; + private int value; + + @Override + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public int getValue() { + return value; + } + public void setValue(int value) { + this.value = value; + } + } + + public static class Impl2 implements TypeBase { + private String name = "Impl2"; + private double decimal; + + @Override + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public double getDecimal() { + return decimal; + } + public void setDecimal(double decimal) { + this.decimal = decimal; + } + } } diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java b/src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java index cec9f304ff..28c1608070 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java @@ -20,18 +20,30 @@ public JsonAssert() { .build(); } + /** Make sure the object will be serialized into the expected JSON */ public void assertJson(JsonTestResource expected, Object actual) { assertJson(loadResource(expected), actual); } + /** Make sure the object will be serialized into the expected JSON */ public void assertJson(JsonNode expected, Object actual) { assertEquals(serialize(expected), serialize(actual)); } + /** Make sure the object will be serialized into the expected JSON */ public void assertSerialized(String expectedJson, Object actual) { assertEquals(expectedJson, serialize(actual)); } + + /** Make sure the object will be serialized into the expected JSON and that it can be deserialized. */ + public void assertStableSerialization(String expectedJson, Object actual, Class baseType) throws IOException { + assertEquals(expectedJson, serialize(actual)); + + Object deserialized = _mapper.readValue(expectedJson, baseType); + assertEquals(expectedJson, serialize(deserialized)); + } + /** Serialize a JsonNode tree */ public String serialize(JsonNode input) { try { return _mapper.writeValueAsString(input); @@ -40,6 +52,7 @@ public String serialize(JsonNode input) { } } + /** Serialize any object. This even works when data is a {@link JsonNode}. */ public String serialize(Object data) { if (data instanceof JsonNode) { return serialize((JsonNode)data); @@ -53,6 +66,7 @@ public String serialize(Object data) { } } + /** Load test data from the classpath into a {@link JsonNode}. */ public JsonNode loadResource(JsonTestResource resource) { try (InputStream stream = resource.getInputStream()) { // TODO Formatting ok? JUnit 4 or 5 here? From 1690cf720e011eb7268a68fe51c07e62e37a55d3 Mon Sep 17 00:00:00 2001 From: Aaron Digulla Date: Mon, 19 Jun 2023 22:27:50 +0200 Subject: [PATCH 7/9] One TODO fixed, two new ones. --- .../jackson/databind/ser/CanonicalNumberGenerator.java | 3 ++- .../java/com/fasterxml/jackson/databind/ser/JsonAssert.java | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberGenerator.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberGenerator.java index a13193c591..116efd944c 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberGenerator.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberGenerator.java @@ -51,12 +51,13 @@ public static boolean verifyBigDecimalRange(BigDecimal value, SerializerProvider return result; } + // TODO Everyone should use the same method; move this to jackson-core. 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 + // TODO Everyone should use the same method; move this to jackson-core. 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]", diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java b/src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java index 28c1608070..fa9fdefc5b 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/JsonAssert.java @@ -69,7 +69,6 @@ public String serialize(Object data) { /** Load test data from the classpath into a {@link JsonNode}. */ 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); From 1e2cd6ab2c4a055975555a5118adbf6867e27596 Mon Sep 17 00:00:00 2001 From: Aaron Digulla Date: Tue, 20 Jun 2023 22:43:44 +0200 Subject: [PATCH 8/9] StdSerializer isn't necessary anymore. --- .../ser/CanonicalBigDecimalSerializer.java | 76 ------------------- .../ser/CanonicalBigDecimalToString.java | 43 +++++++++++ ...a => CanonicalBigDecimalToStringTest.java} | 4 +- .../databind/ser/CanonicalJsonMapper.java | 12 +-- .../databind/ser/CanonicalJsonModule.java | 18 ----- .../databind/ser/CanonicalJsonTest.java | 37 +++++++++ .../CanonicalNumberSerializerProvider.java | 10 --- .../ser/PrettyBigDecimalSerializer.java | 46 ----------- .../ser/PrettyBigDecimalToString.java | 13 ++++ 9 files changed, 101 insertions(+), 158 deletions(-) delete mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializer.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalToString.java rename src/test/java/com/fasterxml/jackson/databind/ser/{CanonicalBigDecimalSerializerTest.java => CanonicalBigDecimalToStringTest.java} (91%) delete mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonModule.java delete mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberSerializerProvider.java delete mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/PrettyBigDecimalSerializer.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/PrettyBigDecimalToString.java diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializer.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializer.java deleted file mode 100644 index 447b0d221b..0000000000 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializer.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.fasterxml.jackson.databind.ser; - -import java.io.IOException; -import java.math.BigDecimal; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; - -public class CanonicalBigDecimalSerializer extends StdSerializer - implements ValueToString { - private static final long serialVersionUID = 1L; - - 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 IOException { - 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/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalToString.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalToString.java new file mode 100644 index 0000000000..3ebf1f0c23 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalToString.java @@ -0,0 +1,43 @@ +package com.fasterxml.jackson.databind.ser; + +import java.math.BigDecimal; + +public class CanonicalBigDecimalToString implements ValueToString { + + public static final CanonicalBigDecimalToString INSTANCE = new CanonicalBigDecimalToString(); + + @Override + public String convert(BigDecimal value) { + 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/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializerTest.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalToStringTest.java similarity index 91% rename from src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializerTest.java rename to src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalToStringTest.java index d49e51acfd..74ed452297 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalSerializerTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalBigDecimalToStringTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test; -public class CanonicalBigDecimalSerializerTest { +public class CanonicalBigDecimalToStringTest { @Test void testCanonicalDecimalHandling_1() throws Exception { @@ -45,7 +45,7 @@ void testCanonicalHugeDecimalHandling() throws Exception { } private void assertSerialized(String expected, BigDecimal actual) { - CanonicalBigDecimalSerializer serializer = new CanonicalBigDecimalSerializer(); + CanonicalBigDecimalToString serializer = new CanonicalBigDecimalToString(); assertEquals(expected, serializer.convert(actual)); } diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java index d9e72c9d92..c9329b0114 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonMapper.java @@ -1,5 +1,7 @@ package com.fasterxml.jackson.databind.ser; +import java.math.BigDecimal; + import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGeneratorDecorator; import com.fasterxml.jackson.core.PrettyPrinter; @@ -21,7 +23,7 @@ public class CanonicalJsonMapper { .withSeparators(Separators.createDefaultInstance().withObjectFieldValueSpacing(Spacing.AFTER)); public static class Builder { - private CanonicalNumberSerializerProvider _numberSerializerProvider = CanonicalBigDecimalSerializer.PROVIDER; + private ValueToString _numberToString = CanonicalBigDecimalToString.INSTANCE; private boolean _enablePrettyPrinting = false; private Builder() { @@ -30,13 +32,12 @@ private Builder() { public Builder prettyPrint() { _enablePrettyPrinting = true; - _numberSerializerProvider = PrettyBigDecimalSerializer.PROVIDER; + _numberToString = PrettyBigDecimalToString.INSTANCE; return this; } public JsonMapper build() { - JsonGeneratorDecorator decorator = new CanonicalJsonGeneratorDecorator(_numberSerializerProvider.getValueToString()); - CanonicalJsonModule module = new CanonicalJsonModule(_numberSerializerProvider.getNumberSerializer()); + JsonGeneratorDecorator decorator = new CanonicalJsonGeneratorDecorator(_numberToString); JsonFactory factory = JsonFactory.builder() // .decorateWith(decorator) @@ -44,8 +45,7 @@ public JsonMapper build() { JsonMapper.Builder builder = JsonMapper.builder(factory) // .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) // .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) // - .enable(JsonNodeFeature.WRITE_PROPERTIES_SORTED) // - .addModule(module); + .enable(JsonNodeFeature.WRITE_PROPERTIES_SORTED); // if (_enablePrettyPrinting) { builder = builder // diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonModule.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonModule.java deleted file mode 100644 index 807c9e0d71..0000000000 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonModule.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.fasterxml.jackson.databind.ser; - -import java.math.BigDecimal; - -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.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/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java index b258a0604b..6129d0f8ac 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java @@ -144,6 +144,32 @@ void testJsonTypeInfoSorting2() throws Exception { TypeBase.class); } + @Test + void testBigDecimalValue() throws Exception { + BigDecimalValue inst = new BigDecimalValue(); + + inst.setValue(BigDecimal.valueOf(0.1)); + JSON_ASSERT.assertStableSerialization( + "{\n" + + " \"value\": 0.1\n" + + "}", + inst, + BigDecimalValue.class); + } + + @Test + void testBigDecimalValue2() throws Exception { + BigDecimalValue inst = new BigDecimalValue(); + + inst.setValue(new BigDecimal("10.0100")); + JSON_ASSERT.assertStableSerialization( + "{\n" + + " \"value\": 10.01\n" + + "}", + inst, + BigDecimalValue.class); + } + private void assertSerialized(String expected, Object input, JsonMapper mapper) { String actual; try { @@ -247,4 +273,15 @@ public void setDecimal(double decimal) { this.decimal = decimal; } } + + public static class BigDecimalValue { + private BigDecimal value; + + public BigDecimal getValue() { + return value; + } + public void setValue(BigDecimal value) { + this.value = value; + } + } } diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberSerializerProvider.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberSerializerProvider.java deleted file mode 100644 index 814e811a62..0000000000 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalNumberSerializerProvider.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.fasterxml.jackson.databind.ser; - -import java.math.BigDecimal; - -import com.fasterxml.jackson.databind.ser.std.StdSerializer; - -public interface CanonicalNumberSerializerProvider { - StdSerializer getNumberSerializer(); - ValueToString getValueToString(); -} diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/PrettyBigDecimalSerializer.java b/src/test/java/com/fasterxml/jackson/databind/ser/PrettyBigDecimalSerializer.java deleted file mode 100644 index 4ef2cee044..0000000000 --- a/src/test/java/com/fasterxml/jackson/databind/ser/PrettyBigDecimalSerializer.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.fasterxml.jackson.databind.ser; - -import java.io.IOException; -import java.math.BigDecimal; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; - -public class PrettyBigDecimalSerializer extends StdSerializer - implements ValueToString { - - private static final long serialVersionUID = 1L; - - 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 IOException { - 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/com/fasterxml/jackson/databind/ser/PrettyBigDecimalToString.java b/src/test/java/com/fasterxml/jackson/databind/ser/PrettyBigDecimalToString.java new file mode 100644 index 0000000000..23edb1a5ae --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/PrettyBigDecimalToString.java @@ -0,0 +1,13 @@ +package com.fasterxml.jackson.databind.ser; + +import java.math.BigDecimal; + +public class PrettyBigDecimalToString implements ValueToString { + + public static final PrettyBigDecimalToString INSTANCE = new PrettyBigDecimalToString(); + + @Override + public String convert(BigDecimal value) { + return value.stripTrailingZeros().toPlainString(); + } +} \ No newline at end of file From e83a68ccf7c0f314aca45eb480c00d0f3f00f64e Mon Sep 17 00:00:00 2001 From: Aaron Digulla Date: Tue, 20 Jun 2023 22:44:34 +0200 Subject: [PATCH 9/9] Test rounding --- .../com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java index 6129d0f8ac..9766ba304b 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/CanonicalJsonTest.java @@ -149,6 +149,7 @@ void testBigDecimalValue() throws Exception { BigDecimalValue inst = new BigDecimalValue(); inst.setValue(BigDecimal.valueOf(0.1)); + assertEquals("0.1", inst.getValue().toString()); JSON_ASSERT.assertStableSerialization( "{\n" + " \"value\": 0.1\n" @@ -162,6 +163,7 @@ void testBigDecimalValue2() throws Exception { BigDecimalValue inst = new BigDecimalValue(); inst.setValue(new BigDecimal("10.0100")); + assertEquals("10.0100", inst.getValue().toString()); JSON_ASSERT.assertStableSerialization( "{\n" + " \"value\": 10.01\n"