diff --git a/serde-api/src/main/java/io/micronaut/serde/Decoder.java b/serde-api/src/main/java/io/micronaut/serde/Decoder.java index 2742f8c8d..d0b91b3e0 100644 --- a/serde-api/src/main/java/io/micronaut/serde/Decoder.java +++ b/serde-api/src/main/java/io/micronaut/serde/Decoder.java @@ -174,6 +174,28 @@ default Character decodeCharNullable() throws IOException { return decodeNull() ? null : decodeChar(); } + /** + * Decodes a number. + * @return The number + * @throws IOException If an unrecoverable error occurs + * @since 2.9.0 + */ + default Number decodeNumber() throws IOException { + return decodeBigDecimal(); + } + + /** + * Equivalent to {@code decodeNull() ? null : decodeNumber()}. + * + * @return The number + * @throws IOException If an unrecoverable error occurs + * @since 2.9.0 + */ + @Nullable + default Number decodeNumberNullable() throws IOException { + return decodeNull() ? null : decodeNumber(); + } + /** * Decodes a int. * @return The int diff --git a/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonPropertySpec.groovy b/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonPropertySpec.groovy index b8744a6a6..bc9129210 100644 --- a/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonPropertySpec.groovy +++ b/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonPropertySpec.groovy @@ -5,6 +5,43 @@ import spock.lang.Unroll class JsonPropertySpec extends JsonCompileSpec { + @Unroll + void "serde Number"(Number number) { + given: + def context = buildContext('example.Test', ''' +package example; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import java.util.Optional; + +@io.micronaut.serde.annotation.Serdeable +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class Test { + public Number foo; +} +''', [:]) + beanUnderTest.@foo = number + + expect: + jsonMapper.readValue(json, typeUnderTest).@foo == result + writeJson(jsonMapper, beanUnderTest) == json + + cleanup: + context.close() + + where: + number || result || json + 123.456 || 123.456 || '{"foo":123.456}' + Double.valueOf(123.123) || Double.valueOf(123.123) || '{"foo":123.123}' + Float.valueOf(123.123) || Double.valueOf(123.123) || '{"foo":123.123}' + BigDecimal.valueOf(123.123) || BigDecimal.valueOf(123.123) || '{"foo":123.123}' + Integer.valueOf(123) || Integer.valueOf(123) || '{"foo":123}' + Short.valueOf((short) 123) || Short.valueOf((short) 123) || '{"foo":123}' + BigInteger.valueOf(123) || BigInteger.valueOf(123) || '{"foo":123}' + + } + void "missing nullable properties are not overwritten"() { given: def context = buildContext('example.Test', ''' diff --git a/serde-jackson/src/main/java/io/micronaut/serde/jackson/JacksonDecoder.java b/serde-jackson/src/main/java/io/micronaut/serde/jackson/JacksonDecoder.java index 14b7becc5..c7057c194 100644 --- a/serde-jackson/src/main/java/io/micronaut/serde/jackson/JacksonDecoder.java +++ b/serde-jackson/src/main/java/io/micronaut/serde/jackson/JacksonDecoder.java @@ -774,11 +774,20 @@ public BigDecimal decodeBigDecimalNullable() throws IOException { } } - private Number decodeNumber() throws IOException { + private Number doDecodeNumber() throws IOException { nextToken(); return parser.getNumberValue(); } + @Override + public Number decodeNumber() throws IOException { + return switch (peekToken()) { + case VALUE_STRING -> decodeBigDecimal(); + case VALUE_NUMBER_INT, VALUE_NUMBER_FLOAT -> doDecodeNumber(); + default -> throw unexpectedToken(JsonToken.VALUE_NUMBER_INT, nextToken()); + }; + } + @Override public byte @NonNull [] decodeBinary() throws IOException { return switch (peekToken()) { @@ -969,7 +978,7 @@ ArbitraryBuilder proceed() throws IOException { return this; } case VALUE_NUMBER_INT, VALUE_NUMBER_FLOAT -> { - put(key, elementDecoder.decodeNumber()); + put(key, elementDecoder.doDecodeNumber()); return this; } case VALUE_TRUE, VALUE_FALSE -> { diff --git a/serde-support/src/main/java/io/micronaut/serde/support/serdes/NumberTypeSerde.java b/serde-support/src/main/java/io/micronaut/serde/support/serdes/NumberTypeSerde.java new file mode 100644 index 000000000..27abad97c --- /dev/null +++ b/serde-support/src/main/java/io/micronaut/serde/support/serdes/NumberTypeSerde.java @@ -0,0 +1,82 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.serde.support.serdes; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.type.Argument; +import io.micronaut.serde.Decoder; +import io.micronaut.serde.Encoder; +import io.micronaut.serde.exceptions.SerdeException; +import io.micronaut.serde.support.SerdeRegistrar; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; + +@Internal +final class NumberTypeSerde implements SerdeRegistrar, NumberSerde { + + @Override + public Argument getType() { + return Argument.of(Number.class); + } + + @Override + public Number deserialize(Decoder decoder, + DecoderContext decoderContext, + Argument type) throws IOException { + return decoder.decodeNumber(); + } + + @Override + public Number deserializeNullable(@NonNull Decoder decoder, @NonNull DecoderContext context, @NonNull Argument type) throws IOException { + return decoder.decodeNumberNullable(); + } + + @Override + public void serialize(Encoder encoder, + EncoderContext context, + Argument type, + Number value) throws IOException { + if (value instanceof Integer integer) { + encoder.encodeInt(integer); + } else if (value instanceof Long aLong) { + encoder.encodeLong(aLong); + } else if (value instanceof Double aDouble) { + encoder.encodeDouble(aDouble); + } else if (value instanceof Float aFloat) { + encoder.encodeFloat(aFloat); + } else if (value instanceof Byte aByte) { + encoder.encodeByte(aByte); + } else if (value instanceof Short aShort) { + encoder.encodeShort(aShort); + } else if (value instanceof BigDecimal bigDecimal) { + encoder.encodeBigDecimal(bigDecimal); + } else if (value instanceof BigInteger bigInteger) { + encoder.encodeBigInteger(bigInteger); + } else { + throw new SerdeException("Unrecognized Number type: " + value.getClass().getName() + " " + value); + } + } + + @Nullable + @Override + public Integer getDefaultValue(@NonNull DecoderContext context, @NonNull Argument type) { + return type.isPrimitive() ? 0 : null; + } +} diff --git a/serde-support/src/main/java/io/micronaut/serde/support/serdes/Serdes.java b/serde-support/src/main/java/io/micronaut/serde/support/serdes/Serdes.java index c6e4f3d88..f0c01358f 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/serdes/Serdes.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/serdes/Serdes.java @@ -98,7 +98,8 @@ public final class Serdes { new PeriodSerde(), new ByteBufferSerde(), new StringArraySerde(), - new OptionalSerde<>() + new OptionalSerde<>(), + new NumberTypeSerde() ); public static void register(SerdeConfiguration serdeConfiguration,