Skip to content

Commit

Permalink
Fix JsonNumber bson serialization (#898)
Browse files Browse the repository at this point in the history
* 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 <denis.s.stepanov@oracle.com>
  • Loading branch information
rorueda and dstepanov authored Oct 29, 2024
1 parent 0ad6e0d commit f8651b3
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ public void encodeDouble(double value) {
@Override
public void encodeBigInteger(BigInteger value) {
encodeBigDecimal(new BigDecimal(value));
postEncodeValue();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"),
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()}.
Expand All @@ -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));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -309,8 +331,12 @@ abstract class AbstractBasicSerdeSpec extends Specification implements JsonSpec,
}

def <T> T serializeDeserialize(T obj) {
return serializeDeserializeAs(obj, Argument.of(obj.getClass()))
}

def <T> 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) {
Expand Down

0 comments on commit f8651b3

Please sign in to comment.