From ac73f9ff80e613c6a322dc421165c6c29a60b4a5 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 26 Jun 2024 09:56:43 +0200 Subject: [PATCH 1/3] Allow to fail on primitives in constructor with `failOnNullForPrimitives=true` --- .../src/main/kotlin/example/NonNullDto.kt | 8 +++ .../src/main/kotlin/example/NullDto.kt | 9 ++++ .../main/kotlin/example/NullPropertyDto.kt | 9 ++++ .../example/SerdeNullableFailOnMissingTest.kt | 50 +++++++++++++++++++ .../test/kotlin/example/SerdeNullableTest.kt | 36 +++++++++++++ .../DefaultDeserializationConfiguration.java | 10 +++- .../config/DeserializationConfiguration.java | 10 ++++ .../support/deserializers/DeserBean.java | 33 ++++++++---- 8 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 doc-examples/example-kotlin-ksp/src/main/kotlin/example/NonNullDto.kt create mode 100644 doc-examples/example-kotlin-ksp/src/main/kotlin/example/NullDto.kt create mode 100644 doc-examples/example-kotlin-ksp/src/main/kotlin/example/NullPropertyDto.kt create mode 100644 doc-examples/example-kotlin-ksp/src/test/kotlin/example/SerdeNullableFailOnMissingTest.kt create mode 100644 doc-examples/example-kotlin-ksp/src/test/kotlin/example/SerdeNullableTest.kt diff --git a/doc-examples/example-kotlin-ksp/src/main/kotlin/example/NonNullDto.kt b/doc-examples/example-kotlin-ksp/src/main/kotlin/example/NonNullDto.kt new file mode 100644 index 000000000..fbbf2d3f6 --- /dev/null +++ b/doc-examples/example-kotlin-ksp/src/main/kotlin/example/NonNullDto.kt @@ -0,0 +1,8 @@ +package example + +import io.micronaut.serde.annotation.Serdeable + +@Serdeable +data class NonNullDto( + val longField: Long, +) diff --git a/doc-examples/example-kotlin-ksp/src/main/kotlin/example/NullDto.kt b/doc-examples/example-kotlin-ksp/src/main/kotlin/example/NullDto.kt new file mode 100644 index 000000000..c5dcd3089 --- /dev/null +++ b/doc-examples/example-kotlin-ksp/src/main/kotlin/example/NullDto.kt @@ -0,0 +1,9 @@ + +package example + +import io.micronaut.serde.annotation.Serdeable + +@Serdeable +data class NullDto( + val longField: Long? = null +) diff --git a/doc-examples/example-kotlin-ksp/src/main/kotlin/example/NullPropertyDto.kt b/doc-examples/example-kotlin-ksp/src/main/kotlin/example/NullPropertyDto.kt new file mode 100644 index 000000000..a854fdfcc --- /dev/null +++ b/doc-examples/example-kotlin-ksp/src/main/kotlin/example/NullPropertyDto.kt @@ -0,0 +1,9 @@ + +package example + +import io.micronaut.serde.annotation.Serdeable + +@Serdeable +class NullPropertyDto { + var longField: Long? = null +} diff --git a/doc-examples/example-kotlin-ksp/src/test/kotlin/example/SerdeNullableFailOnMissingTest.kt b/doc-examples/example-kotlin-ksp/src/test/kotlin/example/SerdeNullableFailOnMissingTest.kt new file mode 100644 index 000000000..83c102dd4 --- /dev/null +++ b/doc-examples/example-kotlin-ksp/src/test/kotlin/example/SerdeNullableFailOnMissingTest.kt @@ -0,0 +1,50 @@ +package example + +import io.micronaut.context.annotation.Property +import io.micronaut.json.JsonMapper +import io.micronaut.serde.exceptions.SerdeException +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +@Property(name = "micronaut.serde.deserialization.failOnNullForPrimitives", value = "true") +@MicronautTest +class SerdeNullableFailOnMissingTest { + + @Test + fun testDefaultValue(objectMapper: JsonMapper) { + val result = objectMapper.writeValueAsString(NullDto()) + val bean = objectMapper.readValue(result, NullDto::class.java) + Assertions.assertEquals(null, bean.longField) + } + + @Test + fun testNonNullValue(objectMapper: JsonMapper) { + val e = Assertions.assertThrows(SerdeException::class.java) { + objectMapper.readValue("{}", NonNullDto::class.java) + } + Assertions.assertEquals( + "Unable to deserialize type [example.NonNullDto]. Required constructor parameter [long longField] at index [0] is not present or is null in the supplied data", + e.message + ) + } + + @Test + fun testNonNullValue2(objectMapper: JsonMapper) { + val e = Assertions.assertThrows(SerdeException::class.java) { + objectMapper.readValue("{\"longField\": null}", NonNullDto::class.java) + } + e.printStackTrace(); + Assertions.assertEquals( + "Unable to deserialize type [example.NonNullDto]. Required constructor parameter [long longField] at index [0] is not present or is null in the supplied data", + e.message + ) + } + + @Test + fun testNullPropertyValue(objectMapper: JsonMapper) { + val bean = objectMapper.readValue("{}", NullPropertyDto::class.java) + Assertions.assertEquals(null, bean.longField) + } + +} diff --git a/doc-examples/example-kotlin-ksp/src/test/kotlin/example/SerdeNullableTest.kt b/doc-examples/example-kotlin-ksp/src/test/kotlin/example/SerdeNullableTest.kt new file mode 100644 index 000000000..ccf64872f --- /dev/null +++ b/doc-examples/example-kotlin-ksp/src/test/kotlin/example/SerdeNullableTest.kt @@ -0,0 +1,36 @@ +package example + +import io.micronaut.json.JsonMapper +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +@MicronautTest +class SerdeNullableTest { + + @Test + fun testDefaultValue(objectMapper: JsonMapper) { + val result = objectMapper.writeValueAsString(NullDto()) + val bean = objectMapper.readValue(result, NullDto::class.java) + Assertions.assertEquals(null, bean.longField) + } + + @Test + fun testNonNullValue(objectMapper: JsonMapper) { + val bean = objectMapper.readValue("{}", NonNullDto::class.java) + Assertions.assertEquals(0, bean.longField) + } + + @Test + fun testNonNullValue2(objectMapper: JsonMapper) { + val bean = objectMapper.readValue("{\"longField\":null}", NonNullDto::class.java) + Assertions.assertEquals(0, bean.longField) + } + + @Test + fun testNullPropertyValue(objectMapper: JsonMapper) { + val bean = objectMapper.readValue("{}", NullPropertyDto::class.java) + Assertions.assertEquals(null, bean.longField) + } + +} diff --git a/serde-api/src/main/java/io/micronaut/serde/config/DefaultDeserializationConfiguration.java b/serde-api/src/main/java/io/micronaut/serde/config/DefaultDeserializationConfiguration.java index 90e350ca2..fc4a098b5 100644 --- a/serde-api/src/main/java/io/micronaut/serde/config/DefaultDeserializationConfiguration.java +++ b/serde-api/src/main/java/io/micronaut/serde/config/DefaultDeserializationConfiguration.java @@ -32,14 +32,17 @@ final class DefaultDeserializationConfiguration implements DeserializationConfig private final boolean ignoreUnknown; private final int arraySizeThreshold; private final boolean strictNullable; + private final boolean failOnNullForPrimitives; @ConfigurationInject DefaultDeserializationConfiguration(@Bindable(defaultValue = StringUtils.TRUE) boolean ignoreUnknown, @Bindable(defaultValue = "100") int arraySizeThreshold, - @Bindable(defaultValue = StringUtils.FALSE) boolean strictNullable) { + @Bindable(defaultValue = StringUtils.FALSE) boolean strictNullable, + @Bindable(defaultValue = StringUtils.FALSE) boolean failOnNullForPrimitives) { this.ignoreUnknown = ignoreUnknown; this.arraySizeThreshold = arraySizeThreshold; this.strictNullable = strictNullable; + this.failOnNullForPrimitives = failOnNullForPrimitives; } @Override @@ -56,4 +59,9 @@ public int getArraySizeThreshold() { public boolean isStrictNullable() { return strictNullable; } + + @Override + public boolean isFailOnNullForPrimitives() { + return failOnNullForPrimitives; + } } diff --git a/serde-api/src/main/java/io/micronaut/serde/config/DeserializationConfiguration.java b/serde-api/src/main/java/io/micronaut/serde/config/DeserializationConfiguration.java index 2ba3cea11..d04fe55dd 100644 --- a/serde-api/src/main/java/io/micronaut/serde/config/DeserializationConfiguration.java +++ b/serde-api/src/main/java/io/micronaut/serde/config/DeserializationConfiguration.java @@ -43,4 +43,14 @@ public interface DeserializationConfiguration { */ @Bindable(defaultValue = StringUtils.FALSE) boolean isStrictNullable(); + + /** + /** + * Whether a null field or a missing value for a primitive should fail the deserialization. Defaults to {@code false} + * @return True if a null field or a missing value for a primitive should fail the deserialization + */ + @Bindable(defaultValue = StringUtils.FALSE) + default boolean isFailOnNullForPrimitives() { + return false; + } } diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java index f0d43795d..c820b9d1b 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java @@ -102,6 +102,7 @@ final class DeserBean { public final int injectPropertiesSize; public final boolean ignoreUnknown; + public final boolean failOnNullForPrimitives; public final boolean delegating; public final boolean simpleBean; public final boolean recordLikeBean; @@ -153,8 +154,11 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio // Replicating Jackson behaviour: @JsonIncludeProperties will ignore any not-included properties boolean hasIncludedProperties = serdeArgumentConf != null && serdeArgumentConf.getIncluded() != null || introspection.isAnnotationPresent(SerdeConfig.SerIncluded.class); + DeserializationConfiguration deserializationConfiguration = decoderContext.getDeserializationConfiguration().orElse(defaultDeserializationConfiguration); this.ignoreUnknown = hasIncludedProperties || introspection.booleanValue(SerdeConfig.SerIgnored.class, SerdeConfig.SerIgnored.IGNORE_UNKNOWN) - .orElse(decoderContext.getDeserializationConfiguration().orElse(defaultDeserializationConfiguration).isIgnoreUnknown()); + .orElse(deserializationConfiguration.isIgnoreUnknown()); + this.failOnNullForPrimitives = deserializationConfiguration.isFailOnNullForPrimitives(); + final PropertiesBag.Builder creatorPropertiesBuilder = new PropertiesBag.Builder<>(introspection, constructorArguments.length); BeanMethod jsonValueMethod = null; @@ -220,7 +224,8 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio null, unwrapped, null, - isIgnored + isIgnored, + failOnNullForPrimitives ); if (isUnwrapped) { if (creatorUnwrapped == null) { @@ -260,7 +265,8 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio null, null, null, - false + false, + failOnNullForPrimitives ); readPropertiesBuilder.register(jsonProperty, derProperty, true); } @@ -335,7 +341,8 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio null, unwrapped, null, - false + false, + failOnNullForPrimitives ); if (isUnwrapped) { if (unwrappedProperties == null) { @@ -376,7 +383,8 @@ public AnnotationMetadata getAnnotationMetadata() { jsonSetter, null, null, - false + false, + failOnNullForPrimitives ); readPropertiesBuilder.register(property, derProperty, true); } @@ -703,6 +711,7 @@ public static final class DerProperty { public final P defaultValue; public final boolean mustSetField; public final boolean explicitlyRequired; + public final boolean explicitlyRequiredForConstructor; public final boolean nonNull; public final boolean nullable; public final boolean isAnySetter; @@ -732,7 +741,8 @@ public static final class DerProperty { @Nullable BeanMethod beanMethod, @Nullable DeserBean

unwrapped, @Nullable DerProperty unwrappedProperty, - boolean ignored) throws SerdeException { + boolean ignored, + boolean failOnNullForPrimitives) throws SerdeException { this(conversionService, introspection, index, @@ -743,7 +753,8 @@ public static final class DerProperty { beanMethod, unwrapped, unwrappedProperty, - ignored + ignored, + failOnNullForPrimitives ); } @@ -757,7 +768,8 @@ public static final class DerProperty { @Nullable BeanMethod beanMethod, @Nullable DeserBean

unwrapped, @Nullable DerProperty unwrappedProperty, - boolean ignored) throws SerdeException { + boolean ignored, + boolean failOnNullForPrimitives) throws SerdeException { this.introspection = introspection; this.index = index; this.argument = argument; @@ -803,6 +815,7 @@ public static final class DerProperty { .orElse(null); this.explicitlyRequired = annotationMetadata.booleanValue(SerdeConfig.class, SerdeConfig.REQUIRED) .orElse(false); + this.explicitlyRequiredForConstructor = explicitlyRequired || argument.isPrimitive() && failOnNullForPrimitives; } public void setDefaultPropertyValue(Deserializer.DecoderContext decoderContext, @NonNull B bean) throws SerdeException { @@ -817,7 +830,7 @@ public void setDefaultPropertyValue(Deserializer.DecoderContext decoderContext, } public void setDefaultConstructorValue(Deserializer.DecoderContext decoderContext, @NonNull Object[] params) throws SerdeException { - if (explicitlyRequired) { + if (explicitlyRequiredForConstructor) { throw new SerdeException("Unable to deserialize type [" + introspection.getBeanType().getName() + "]. Required constructor parameter [" + argument + "] at index [" + index + "] is not present or is null in the supplied data"); } params[index] = provideDefaultValue(decoderContext, mustSetField || argument.isPrimitive()); @@ -841,7 +854,7 @@ public void deserializeAndSetConstructorValue(Decoder objectDecoder, Deserialize } } - @NextMajorVersion("Receiving a null value for a primitive or a non-null should produce an expection") + @NextMajorVersion("Receiving a null value for a primitive or a non-null should produce an exception") public void deserializeAndSetPropertyValue(Decoder objectDecoder, Deserializer.DecoderContext decoderContext, B beanInstance) throws IOException { try { P value = deserializeValue(objectDecoder, decoderContext); From a7095b7f44cebbba7f674bd6ee17eb75ce32503c Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 26 Jun 2024 10:03:28 +0200 Subject: [PATCH 2/3] Improve --- .../io/micronaut/serde/support/deserializers/DeserBean.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java index c820b9d1b..df931c97c 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java @@ -710,6 +710,7 @@ public static final class DerProperty { @Nullable public final P defaultValue; public final boolean mustSetField; + public final boolean mustSetFieldForConstructor; public final boolean explicitlyRequired; public final boolean explicitlyRequiredForConstructor; public final boolean nonNull; @@ -779,6 +780,7 @@ public static final class DerProperty { || type.equals(OptionalLong.class) || type.equals(OptionalDouble.class) || type.equals(OptionalInt.class); + this.mustSetFieldForConstructor = mustSetField || argument.isPrimitive(); this.nonNull = argument.isNonNull(); this.nullable = argument.isNullable(); if (beanProperty != null) { @@ -833,7 +835,7 @@ public void setDefaultConstructorValue(Deserializer.DecoderContext decoderContex if (explicitlyRequiredForConstructor) { throw new SerdeException("Unable to deserialize type [" + introspection.getBeanType().getName() + "]. Required constructor parameter [" + argument + "] at index [" + index + "] is not present or is null in the supplied data"); } - params[index] = provideDefaultValue(decoderContext, mustSetField || argument.isPrimitive()); + params[index] = provideDefaultValue(decoderContext, mustSetFieldForConstructor); } public void set(@NonNull Deserializer.DecoderContext decoderContext, @NonNull B obj, @Nullable P value) throws SerdeException { From 9c55038ab3ed503f4c516df3a25450678bb5fef7 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Wed, 26 Jun 2024 15:43:06 +0200 Subject: [PATCH 3/3] Update serde-api/src/main/java/io/micronaut/serde/config/DeserializationConfiguration.java Co-authored-by: Jonas Konrad --- .../io/micronaut/serde/config/DeserializationConfiguration.java | 1 - 1 file changed, 1 deletion(-) diff --git a/serde-api/src/main/java/io/micronaut/serde/config/DeserializationConfiguration.java b/serde-api/src/main/java/io/micronaut/serde/config/DeserializationConfiguration.java index d04fe55dd..71a3c9729 100644 --- a/serde-api/src/main/java/io/micronaut/serde/config/DeserializationConfiguration.java +++ b/serde-api/src/main/java/io/micronaut/serde/config/DeserializationConfiguration.java @@ -45,7 +45,6 @@ public interface DeserializationConfiguration { boolean isStrictNullable(); /** - /** * Whether a null field or a missing value for a primitive should fail the deserialization. Defaults to {@code false} * @return True if a null field or a missing value for a primitive should fail the deserialization */