From ba9e4834fadd8870b5881946e8074ce77c988014 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Tue, 2 Apr 2024 16:30:20 +0200 Subject: [PATCH] Support ignoring constructor/record parameter (#809) --- ...rdeableWithIgnoredNonSerdeableFieldTest.kt | 27 +++++++ .../serde/jackson/JsonIgnoreSpec.groovy | 30 ++++++++ .../annotation/SerdeJsonIgnoreSpec.groovy | 72 ++++++++++++++++++ .../support/deserializers/DeserBean.java | 34 ++++++--- .../databind/DatabindJsonIgnoreSpec.groovy | 75 +++++++++++++++++++ 5 files changed, 228 insertions(+), 10 deletions(-) create mode 100644 doc-examples/example-kotlin/src/test/kotlin/example/SerdeableWithIgnoredNonSerdeableFieldTest.kt diff --git a/doc-examples/example-kotlin/src/test/kotlin/example/SerdeableWithIgnoredNonSerdeableFieldTest.kt b/doc-examples/example-kotlin/src/test/kotlin/example/SerdeableWithIgnoredNonSerdeableFieldTest.kt new file mode 100644 index 000000000..ec1e60371 --- /dev/null +++ b/doc-examples/example-kotlin/src/test/kotlin/example/SerdeableWithIgnoredNonSerdeableFieldTest.kt @@ -0,0 +1,27 @@ +package example + +import com.fasterxml.jackson.annotation.JsonIgnore +import io.micronaut.serde.ObjectMapper +import io.micronaut.serde.annotation.Serdeable +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Test + +@MicronautTest +class SerdeableWithIgnoredNonSerdeableFieldTest { + + @Test + fun test(objectMapper: ObjectMapper) { + val original = SerdeableWithIgnoredNonSerdeable(NonSerdeable("value")) + + val serialized = objectMapper.writeValueAsString(original) + val deserialized = objectMapper.readValue(serialized, SerdeableWithIgnoredNonSerdeable::class.java) + + assert(deserialized == original.copy(ignoredNonSerdeable = null)) + } + +} + +@Serdeable +data class SerdeableWithIgnoredNonSerdeable(@field:JsonIgnore val ignoredNonSerdeable: NonSerdeable? = null) + +data class NonSerdeable(val value: String) diff --git a/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonIgnoreSpec.groovy b/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonIgnoreSpec.groovy index f3cf3627f..d5ec1d873 100644 --- a/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonIgnoreSpec.groovy +++ b/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonIgnoreSpec.groovy @@ -69,6 +69,36 @@ class Test { context.close() } + void "json ignore on a bean"() { + given: + def context = buildContext('example.Test', ''' +package example; + +import com.fasterxml.jackson.annotation.*; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; + +@JsonIgnoreProperties(ignoreUnknown = true) +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class Test { + @JsonIgnore + public Ignored foo; + public String bar; +} +class Ignored { +} +''', [:]) + + def des = jsonMapper.readValue('{"foo": "1", "bar": "2"}', typeUnderTest) + + expect: + des.foo == null + des.bar == "2" + + cleanup: + context.close() + } + void "test @JsonIgnoreType"() { given: def context = buildContext(""" diff --git a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonIgnoreSpec.groovy b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonIgnoreSpec.groovy index 3a09b7552..5b252e3ac 100644 --- a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonIgnoreSpec.groovy +++ b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonIgnoreSpec.groovy @@ -8,4 +8,76 @@ class SerdeJsonIgnoreSpec extends JsonIgnoreSpec { protected String unknownPropertyMessage(String propertyName, String className) { return "Unknown property [$propertyName] encountered during deserialization of type: ${NameUtils.getSimpleName(className)}" } + + void "json ignore on a constructor parameter"() { + given: + def context = buildContext('example.Test', ''' +package example; + +import com.fasterxml.jackson.annotation.*; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.serde.annotation.Serdeable; + +@JsonIgnoreProperties(ignoreUnknown = true) +class Test{ + @JsonIgnore + private final Ignored foo; + private final String bar; + + @JsonCreator + public Test(@JsonProperty("foo") Ignored foo, @JsonProperty("bar") String bar) { + this.foo = foo; + this.bar = bar; + } + + public example.Ignored getFoo() { + return foo; + } + + public String getBar() { + return bar; + } + +} +class Ignored { +} +''') + + def des = jsonMapper.readValue('{"foo": "1", "bar": "2"}', typeUnderTest) + + expect: + des.foo == null + des.bar == "2" + + cleanup: + context.close() + } + + void "json ignore on a record parameter"() { + given: + def context = buildContext('example.Test', ''' +package example; + +import com.fasterxml.jackson.annotation.*; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.serde.annotation.Serdeable; + +@JsonIgnoreProperties(ignoreUnknown = true) +record Test(@JsonIgnore @JsonProperty("foo") Ignored foo, @JsonProperty("bar") String bar) { +} +class Ignored { +} +''') + + def des = jsonMapper.readValue('{"foo": "1", "bar": "2"}', typeUnderTest) + + expect: + des.foo == null + des.bar == "2" + + cleanup: + context.close() + } } 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 3ecb18e37..b82e8a5d9 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 @@ -186,7 +186,8 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio PropertyNamingStrategy propertyNamingStrategy = getPropertyNamingStrategy(annotationMetadata, decoderContext, entityPropertyNamingStrategy); final String propertyName = resolveName(serdeArgumentConf, constructorArgument, annotationMetadata, propertyNamingStrategy); - if (isIgnored(annotationMetadata) || (allowPropertyPredicate != null && !allowPropertyPredicate.test(propertyName))) { + boolean isIgnored = isIgnored(annotationMetadata) || (allowPropertyPredicate != null && !allowPropertyPredicate.test(propertyName)); + if (isIgnored) { ignoredProperties.add(propertyName); } @@ -210,10 +211,13 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio i, propertyName, constructorWithPropertyArgument, - introspection.getProperty(propertyName).orElse(null), + isIgnored ? null : introspection.getProperty(propertyName) + .or(() -> introspection.getProperty(constructorArgument.getName())) + .orElse(null), null, unwrapped, - null + null, + isIgnored ); if (isUnwrapped) { if (creatorUnwrapped == null) { @@ -252,7 +256,8 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio null, null, null, - null + null, + false ); readPropertiesBuilder.register(jsonProperty, derProperty, true); } @@ -326,7 +331,8 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio beanProperty, null, unwrapped, - null + null, + false ); if (isUnwrapped) { if (unwrappedProperties == null) { @@ -366,7 +372,8 @@ public AnnotationMetadata getAnnotationMetadata() { null, jsonSetter, null, - null + null, + false ); readPropertiesBuilder.register(property, derProperty, true); } @@ -481,7 +488,9 @@ private boolean isRecordLikeBean() { } private void initProperty(DerProperty property, Deserializer.DecoderContext decoderContext) throws SerdeException { - property.deserializer = findDeserializer(decoderContext, property.argument); + if (!property.ignored) { + property.deserializer = findDeserializer(decoderContext, property.argument); + } } private PropertyNamingStrategy getPropertyNamingStrategy(AnnotationMetadata annotationMetadata, @@ -703,6 +712,7 @@ public static final class DerProperty { public final DerProperty unwrappedProperty; public final String managedRef; public final String backRef; + public final boolean ignored; // Null when DeserBean not initialized public Deserializer

deserializer; @@ -715,7 +725,8 @@ public static final class DerProperty { @Nullable BeanWriteProperty beanProperty, @Nullable BeanMethod beanMethod, @Nullable DeserBean

unwrapped, - @Nullable DerProperty unwrappedProperty) throws SerdeException { + @Nullable DerProperty unwrappedProperty, + boolean ignored) throws SerdeException { this(conversionService, introspection, index, @@ -725,7 +736,8 @@ public static final class DerProperty { beanProperty, beanMethod, unwrapped, - unwrappedProperty + unwrappedProperty, + ignored ); } @@ -738,10 +750,12 @@ public static final class DerProperty { @Nullable BeanWriteProperty beanProperty, @Nullable BeanMethod beanMethod, @Nullable DeserBean

unwrapped, - @Nullable DerProperty unwrappedProperty) throws SerdeException { + @Nullable DerProperty unwrappedProperty, + boolean ignored) throws SerdeException { this.introspection = introspection; this.index = index; this.argument = argument; + this.ignored = ignored; Class type = argument.getType(); this.mustSetField = argument.isNonNull() || type.equals(Optional.class) || type.equals(OptionalLong.class) diff --git a/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonIgnoreSpec.groovy b/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonIgnoreSpec.groovy index 24d2804bd..178859520 100644 --- a/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonIgnoreSpec.groovy +++ b/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonIgnoreSpec.groovy @@ -2,6 +2,7 @@ package io.micronaut.serde.tck.jackson.databind import io.micronaut.context.ApplicationContextBuilder import io.micronaut.serde.jackson.JsonIgnoreSpec +import spock.lang.PendingFeature class DatabindJsonIgnoreSpec extends JsonIgnoreSpec { @@ -17,4 +18,78 @@ class DatabindJsonIgnoreSpec extends JsonIgnoreSpec { return """Unrecognized field "$propertyName" (class $className), not marked as ignorable""" } + @PendingFeature(reason = "Jackson doesn't support ignored record values") + void "json ignore on a record parameter"() { + given: + def context = buildContext('example.Test', ''' +package example; + +import com.fasterxml.jackson.annotation.*; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.serde.annotation.Serdeable; + +@JsonIgnoreProperties(ignoreUnknown = true) +record Test(@JsonIgnore @JsonProperty("foo") Ignored foo, @JsonProperty("bar") String bar) { +} +class Ignored { +} +''') + + def des = jsonMapper.readValue('{"foo": {}, "bar": "2"}', typeUnderTest) + + expect: + des.foo == null + des.bar == "2" + + cleanup: + context.close() + } + + @PendingFeature(reason = "Jackson doesn't support ignored constructor values") + void "json ignore on a constructor parameter"() { + given: + def context = buildContext('example.Test', ''' +package example; + +import com.fasterxml.jackson.annotation.*; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.serde.annotation.Serdeable; + +@JsonIgnoreProperties(ignoreUnknown = true) +class Test{ + @JsonIgnore + private final Ignored foo; + private final String bar; + + @JsonCreator + public Test(@JsonProperty("foo") Ignored foo, @JsonProperty("bar") String bar) { + this.foo = foo; + this.bar = bar; + } + + public example.Ignored getFoo() { + return foo; + } + + public String getBar() { + return bar; + } + +} +class Ignored { +} +''') + + def des = jsonMapper.readValue('{"foo": {}, "bar": "2"}', typeUnderTest) + + expect: + des.foo == null + des.bar == "2" + + cleanup: + context.close() + } + }