Skip to content

Commit

Permalink
Merge pull request #895 from micronaut-projects/2.10.x
Browse files Browse the repository at this point in the history
Merge 2.10.x
  • Loading branch information
graemerocher authored Jul 22, 2024
2 parents 03f5c90 + b948b55 commit b69b207
Show file tree
Hide file tree
Showing 20 changed files with 537 additions and 77 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package example

import io.micronaut.serde.annotation.Serdeable

@Serdeable
data class NonNullDto(
val longField: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

package example

import io.micronaut.serde.annotation.Serdeable

@Serdeable
data class NullDto(
val longField: Long? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

package example

import io.micronaut.serde.annotation.Serdeable

@Serdeable
class NullPropertyDto {
var longField: Long? = null
}
Original file line number Diff line number Diff line change
@@ -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)
}

}
Original file line number Diff line number Diff line change
@@ -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)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -56,4 +59,9 @@ public int getArraySizeThreshold() {
public boolean isStrictNullable() {
return strictNullable;
}

@Override
public boolean isFailOnNullForPrimitives() {
return failOnNullForPrimitives;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,13 @@ 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;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.micronaut.serde.jackson.errors


import io.micronaut.json.tree.JsonNode
import io.micronaut.serde.ObjectMapper
import io.micronaut.serde.exceptions.SerdeException
import io.micronaut.test.extensions.spock.annotation.MicronautTest
Expand All @@ -19,7 +19,36 @@ class NoSerdeSpec extends Specification {
def e = thrown(SerdeException)
e.message == 'No serializable introspection present for type Foo. Consider adding Serdeable. Serializable annotate to type Foo. Alternatively if you are not in control of the project\'s source code, you can use @SerdeImport(Foo.class) to enable serialization of this type.'
}

void "test NPE"() {
when:
objectMapper.readValue("{}", WithNpe)

then:
def e = thrown(SerdeException)
e.message == 'Error deserializing type: WithNpe'
}

void "test NPE 2"() {
when:
objectMapper.updateValueFromTree(new WithNpe("noNPE"), JsonNode.from(Map.of()))

then:
def e = thrown(SerdeException)
e.message == 'Error deserializing value: WithNpeToString of type: WithNpe'
}

void "test NPE 3"() {
when:
objectMapper.writeValueAsString(new WithNpe("noNPE"))

then:
def e = thrown(SerdeException)
e.message == 'Error serializing value at path: '
}

static class Foo {}

}


Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.micronaut.serde.jackson.errors;

import io.micronaut.serde.annotation.Serdeable;

@Serdeable
public class WithNpe {

private final String someString;

public WithNpe(String someString) {
this.someString = someString;
if (!"noNPE".equals(someString)) {
throw new NullPointerException("Simulating NPE in constructor");
}
}

public String getSomeString() {
if (true) {
throw new NullPointerException("Simulating NPE in getter");
}
return someString;
}

@Override
public String toString() {
return "WithNpeToString";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ final class DeserBean<T> {
public final int injectPropertiesSize;

public final boolean ignoreUnknown;
public final boolean failOnNullForPrimitives;
public final boolean delegating;
public final boolean simpleBean;
public final boolean recordLikeBean;
Expand Down Expand Up @@ -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<T> creatorPropertiesBuilder = new PropertiesBag.Builder<>(introspection, constructorArguments.length);

BeanMethod<T, Object> jsonValueMethod = null;
Expand Down Expand Up @@ -220,7 +224,8 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio
null,
unwrapped,
null,
isIgnored
isIgnored,
failOnNullForPrimitives
);
if (isUnwrapped) {
if (creatorUnwrapped == null) {
Expand Down Expand Up @@ -260,7 +265,8 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio
null,
null,
null,
false
false,
failOnNullForPrimitives
);
readPropertiesBuilder.register(jsonProperty, derProperty, true);
}
Expand Down Expand Up @@ -335,7 +341,8 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio
null,
unwrapped,
null,
false
false,
failOnNullForPrimitives
);
if (isUnwrapped) {
if (unwrappedProperties == null) {
Expand Down Expand Up @@ -376,7 +383,8 @@ public AnnotationMetadata getAnnotationMetadata() {
jsonSetter,
null,
null,
false
false,
failOnNullForPrimitives
);
readPropertiesBuilder.register(property, derProperty, true);
}
Expand Down Expand Up @@ -702,7 +710,9 @@ public static final class DerProperty<B, P> {
@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;
public final boolean nullable;
public final boolean isAnySetter;
Expand Down Expand Up @@ -732,7 +742,8 @@ public static final class DerProperty<B, P> {
@Nullable BeanMethod<B, P> beanMethod,
@Nullable DeserBean<P> unwrapped,
@Nullable DerProperty<?, ?> unwrappedProperty,
boolean ignored) throws SerdeException {
boolean ignored,
boolean failOnNullForPrimitives) throws SerdeException {
this(conversionService,
introspection,
index,
Expand All @@ -743,7 +754,8 @@ public static final class DerProperty<B, P> {
beanMethod,
unwrapped,
unwrappedProperty,
ignored
ignored,
failOnNullForPrimitives
);
}

Expand All @@ -757,7 +769,8 @@ public static final class DerProperty<B, P> {
@Nullable BeanMethod<B, P> beanMethod,
@Nullable DeserBean<P> unwrapped,
@Nullable DerProperty<?, ?> unwrappedProperty,
boolean ignored) throws SerdeException {
boolean ignored,
boolean failOnNullForPrimitives) throws SerdeException {
this.introspection = introspection;
this.index = index;
this.argument = argument;
Expand All @@ -767,6 +780,7 @@ public static final class DerProperty<B, P> {
|| 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) {
Expand Down Expand Up @@ -803,6 +817,7 @@ public static final class DerProperty<B, P> {
.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 {
Expand All @@ -817,10 +832,10 @@ 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());
params[index] = provideDefaultValue(decoderContext, mustSetFieldForConstructor);
}

public void set(@NonNull Deserializer.DecoderContext decoderContext, @NonNull B obj, @Nullable P value) throws SerdeException {
Expand All @@ -841,7 +856,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);
Expand Down
Loading

0 comments on commit b69b207

Please sign in to comment.