From f8651b398fbf1d41585bc77a8c9e2cb5e38c7cc0 Mon Sep 17 00:00:00 2001 From: rorueda Date: Tue, 29 Oct 2024 05:13:33 -0300 Subject: [PATCH] Fix JsonNumber bson serialization (#898) * Fix JsonNumber bson serialization - Always use the right number type when serializing, so formats that support the type info can deserialize to the correct type - Fix fallback deserialization of custom Number implementations - Fix element index increment when serializing BigInteger * Improve `getBigDecimalFromNumber` * Correct --------- Co-authored-by: Denis Stepanov --- .../serde/bson/BsonReaderDecoder.java | 8 ++++++ .../serde/bson/BsonWriterEncoder.java | 1 - .../bson/BsonBinaryBasicSerdeSpec.groovy | 20 +++++++++++++ .../serde/support/AbstractStreamDecoder.java | 24 +++++++++++++++- .../serde/support/serdes/JsonNodeSerde.java | 8 +++++- .../serde/AbstractBasicSerdeSpec.groovy | 28 ++++++++++++++++++- 6 files changed, 85 insertions(+), 4 deletions(-) diff --git a/serde-bson/src/main/java/io/micronaut/serde/bson/BsonReaderDecoder.java b/serde-bson/src/main/java/io/micronaut/serde/bson/BsonReaderDecoder.java index 843709b97..6f3d78718 100644 --- a/serde-bson/src/main/java/io/micronaut/serde/bson/BsonReaderDecoder.java +++ b/serde-bson/src/main/java/io/micronaut/serde/bson/BsonReaderDecoder.java @@ -279,6 +279,14 @@ protected Number getBestNumber() { }; } + @Override + protected BigDecimal getBigDecimalFromNumber(Number number) { + if (number instanceof Decimal128 decimal128) { + return decimal128.bigDecimalValue(); + } + return super.getBigDecimalFromNumber(number); + } + @Override public byte @NonNull [] decodeBinary() throws IOException { if (currentBsonType == BsonType.BINARY) { diff --git a/serde-bson/src/main/java/io/micronaut/serde/bson/BsonWriterEncoder.java b/serde-bson/src/main/java/io/micronaut/serde/bson/BsonWriterEncoder.java index 1e74bdbf4..d1e682464 100644 --- a/serde-bson/src/main/java/io/micronaut/serde/bson/BsonWriterEncoder.java +++ b/serde-bson/src/main/java/io/micronaut/serde/bson/BsonWriterEncoder.java @@ -150,7 +150,6 @@ public void encodeDouble(double value) { @Override public void encodeBigInteger(BigInteger value) { encodeBigDecimal(new BigDecimal(value)); - postEncodeValue(); } @Override diff --git a/serde-bson/src/test/groovy/io/micronaut/serde/bson/BsonBinaryBasicSerdeSpec.groovy b/serde-bson/src/test/groovy/io/micronaut/serde/bson/BsonBinaryBasicSerdeSpec.groovy index 0e1d6e215..7e02b8cab 100644 --- a/serde-bson/src/test/groovy/io/micronaut/serde/bson/BsonBinaryBasicSerdeSpec.groovy +++ b/serde-bson/src/test/groovy/io/micronaut/serde/bson/BsonBinaryBasicSerdeSpec.groovy @@ -2,6 +2,7 @@ package io.micronaut.serde.bson import io.micronaut.core.type.Argument import io.micronaut.json.JsonMapper +import io.micronaut.json.tree.JsonNode import io.micronaut.serde.AbstractBasicSerdeSpec import io.micronaut.test.extensions.spock.annotation.MicronautTest import jakarta.inject.Inject @@ -46,4 +47,23 @@ class BsonBinaryBasicSerdeSpec extends AbstractBasicSerdeSpec implements BsonBin assert result == expected return result == expected } + + def "validate json node including type"() { + when: + def result = serializeDeserializeAs( + JsonNode.createObjectNode(["v": jsonNode]), Argument.of(JsonNode.class)).get("v") + then: + result.value == jsonNode.value && result.value.class == jsonNode.value.class + + where: + // the type doesn't match for float and big integer as bson encodes them as double and big decimal + jsonNode << [ + JsonNode.createBooleanNode(true), + JsonNode.createNumberNode(123), + JsonNode.createNumberNode(234L), + JsonNode.createNumberNode(123.234D), + JsonNode.createNumberNode(BigDecimal.valueOf(12345.12345)), + JsonNode.createStringNode("Hello"), + ] + } } diff --git a/serde-support/src/main/java/io/micronaut/serde/support/AbstractStreamDecoder.java b/serde-support/src/main/java/io/micronaut/serde/support/AbstractStreamDecoder.java index 31c14ee36..34cbf66db 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/AbstractStreamDecoder.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/AbstractStreamDecoder.java @@ -402,6 +402,28 @@ protected int getInteger() throws IOException { */ protected abstract Number getBestNumber() throws IOException; + /** + * Converts the number, probably retrieved by calling {@link #getBestNumber()}, to a {@link BigDecimal}. + * + * @param number The number value + * @return The number as a big decimal + */ + protected BigDecimal getBigDecimalFromNumber(Number number) { + if (number instanceof BigDecimal bigDecimal) { + return bigDecimal; + } + if (number instanceof BigInteger bigInteger) { + return new BigDecimal(bigInteger); + } + if (number instanceof Double aDouble) { + return new BigDecimal(aDouble); + } + if (number instanceof Float aFloat) { + return new BigDecimal(aFloat); + } + return new BigDecimal(number.longValue()); + } + /** * Decode the current {@link TokenType#NUMBER} value as a numeric {@link JsonNode}. Called for no other token type. * Default implementation tries to construct a node from {@link #getBestNumber()}. @@ -424,7 +446,7 @@ protected JsonNode getBestNumberNode() throws IOException { return JsonNode.createNumberNode((BigDecimal) number); } else { // fallback, unknown number type - return JsonNode.createNumberNode(getBigDecimal()); + return JsonNode.createNumberNode(getBigDecimalFromNumber(number)); } } diff --git a/serde-support/src/main/java/io/micronaut/serde/support/serdes/JsonNodeSerde.java b/serde-support/src/main/java/io/micronaut/serde/support/serdes/JsonNodeSerde.java index 35e668428..0584a735e 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/serdes/JsonNodeSerde.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/serdes/JsonNodeSerde.java @@ -48,8 +48,14 @@ private void serialize0(Encoder encoder, JsonNode value) throws IOException { encoder.encodeString(value.getStringValue()); } else if (value.isNumber()) { Number numberValue = value.getNumberValue(); - if (numberValue instanceof Integer || numberValue instanceof Byte || numberValue instanceof Short || numberValue instanceof Long) { + if (numberValue instanceof Integer) { + encoder.encodeInt(numberValue.intValue()); + } else if (numberValue instanceof Long) { encoder.encodeLong(numberValue.longValue()); + } else if (numberValue instanceof Short) { + encoder.encodeShort(numberValue.shortValue()); + } else if (numberValue instanceof Byte) { + encoder.encodeByte(numberValue.byteValue()); } else if (numberValue instanceof BigInteger bi) { encoder.encodeBigInteger(bi); } else if (numberValue instanceof BigDecimal bd) { diff --git a/serde-tck/src/main/groovy/io/micronaut/serde/AbstractBasicSerdeSpec.groovy b/serde-tck/src/main/groovy/io/micronaut/serde/AbstractBasicSerdeSpec.groovy index 3abdc94f2..3fcbc7d41 100644 --- a/serde-tck/src/main/groovy/io/micronaut/serde/AbstractBasicSerdeSpec.groovy +++ b/serde-tck/src/main/groovy/io/micronaut/serde/AbstractBasicSerdeSpec.groovy @@ -17,6 +17,7 @@ package io.micronaut.serde import io.micronaut.core.type.Argument import io.micronaut.json.JsonMapper +import io.micronaut.json.tree.JsonNode import io.micronaut.serde.config.annotation.SerdeConfig import io.micronaut.serde.data.Users1 import io.micronaut.serde.data.Users2 @@ -221,6 +222,27 @@ abstract class AbstractBasicSerdeSpec extends Specification implements JsonSpec, result.bigInteger == BigInteger.valueOf(123456789) } + def "validate json node"() { + when: + def result = serializeDeserializeAs( + JsonNode.createObjectNode(["v": jsonNode]), Argument.of(JsonNode.class)).get("v") + then: + // note that the type of the result may differ from the original as json + // doesn't keep this information + result.value == jsonNode.value + where: + jsonNode << [ + JsonNode.createBooleanNode(true), + JsonNode.createNumberNode(123), + JsonNode.createNumberNode(234L), + JsonNode.createNumberNode(11.22f), + JsonNode.createNumberNode(123.234D), + JsonNode.createNumberNode(BigInteger.valueOf(123456789)), + JsonNode.createNumberNode(BigDecimal.valueOf(12345.12345)), + JsonNode.createStringNode("Hello"), + ] + } + def "should skip unknown values"() { when: def all = jsonMapper.readValue(jsonAsBytes("""{"unknown":"ABC"}"""), Argument.of(AllTypesBean)) @@ -309,8 +331,12 @@ abstract class AbstractBasicSerdeSpec extends Specification implements JsonSpec, } def T serializeDeserialize(T obj) { + return serializeDeserializeAs(obj, Argument.of(obj.getClass())) + } + + def T serializeDeserializeAs(T obj, Argument type) { def output = jsonMapper.writeValueAsBytes(obj) - return jsonMapper.readValue(output, Argument.of(obj.getClass())) as T + return jsonMapper.readValue(output, type) as T } byte[] jsonAsBytes(String json) {