From 8dd9e6d8c167285e157267d1bd81975b97c76a30 Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Fri, 23 Feb 2024 11:35:54 +0100 Subject: [PATCH] Correct ignored properties for a constructor injection (#773) --- .../serde/jackson/JsonPropertySpec.groovy | 85 ++++++++++++++++++ .../annotation/SerdeJsonPropertySpec.groovy | 90 ++++++++++++++++++- .../support/deserializers/DeserBean.java | 11 +-- .../databind/DatabindJsonPropertySpec.groovy | 88 ++++++++++++++++++ 4 files changed, 264 insertions(+), 10 deletions(-) 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 bc9129210..abffee9f6 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,91 @@ import spock.lang.Unroll class JsonPropertySpec extends JsonCompileSpec { + void "test @JsonProperty.Access.WRITE_ONLY (set only) - records"() { + given: + def context = buildContext(""" +package test; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +record Test( + @JsonProperty + String value, + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + String ignored +) {} +""") + when: + def bean = newInstance(context, 'test.Test', "test", "xyz") + def result = writeJson(jsonMapper, bean) + + then: + result == '{"value":"test"}' + + when: + bean = jsonMapper.readValue('{"value":"test","ignored":"xyz"}', argumentOf(context, 'test.Test')) + + then: + bean.value == 'test' + bean.ignored == 'xyz' + + cleanup: + context.close() + } + + void "test @JsonProperty.Access.WRITE_ONLY (set only) - constructor"() { + given: + def context = buildContext(""" +package test; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +class Test { + + private String value; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String ignored; + + @JsonCreator + public Test(@JsonProperty("value") String value, @JsonProperty("ignored") String ignored) { + this.value = value; + this.ignored = ignored; + } + + public String getValue() { + return this.value; + } + + public String getIgnored() { + return this.ignored; + } + +} +""") + when: + def bean = newInstance(context, 'test.Test', "test", "xyz") + def result = writeJson(jsonMapper, bean) + + then: + result == '{"value":"test"}' + + when: + bean = jsonMapper.readValue('{"value":"test","ignored":"xyz"}', argumentOf(context, 'test.Test')) + + then: + bean.value == 'test' + bean.ignored == 'xyz' + + cleanup: + context.close() + } + + @Unroll void "serde Number"(Number number) { given: diff --git a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonPropertySpec.groovy b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonPropertySpec.groovy index 73ea8bc3d..70f282415 100644 --- a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonPropertySpec.groovy +++ b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonPropertySpec.groovy @@ -6,6 +6,92 @@ import spock.lang.PendingFeature class SerdeJsonPropertySpec extends JsonPropertySpec { + void "test @JsonProperty.Access.READ_ONLY (get only) - constructor"() { + // Jackson cannot deserialize READ_ONLY as null + given: + def context = buildContext(""" +package test; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +class Test { + + private String value; + private String ignored; + + @JsonCreator + public Test(@JsonProperty("value") String value, @JsonProperty(value = "ignored", access = JsonProperty.Access.READ_ONLY) String ignored) { + this.value = value; + this.ignored = ignored; + } + + public String getValue() { + return this.value; + } + + public String getIgnored() { + return this.ignored; + } + +} +""") + when: + def bean = newInstance(context, 'test.Test', "test", "xyz") + def result = writeJson(jsonMapper, bean) + + then: + result == '{"value":"test","ignored":"xyz"}' + + when: + bean = jsonMapper.readValue('{"value":"test","ignored":"xyz"}', argumentOf(context, 'test.Test')) + + then: + bean.value == 'test' + bean.ignored == null + + cleanup: + context.close() + } + + void "test @JsonProperty.Access.READ_ONLY (get only) - record"() { + // Jackson cannot deserialize READ_ONLY as null + given: + def context = buildContext(""" +package test; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +record Test( + @JsonProperty + String value, + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + String ignored +) {} +""") + when: + def bean = newInstance(context, 'test.Test', "test", "xyz") + def result = writeJson(jsonMapper, bean) + + then: + result == '{"value":"test","ignored":"xyz"}' + + when: + bean = jsonMapper.readValue('{"value":"test","ignored":"xyz"}', argumentOf(context, 'test.Test')) + + then: + bean.value == 'test' + bean.ignored == null + + cleanup: + context.close() + } + void "test optional by default primitive field in constructor XXX"() { given: @@ -238,7 +324,7 @@ import io.micronaut.serde.annotation.Serdeable; record Test( @JsonProperty(value = "other", defaultValue = "default") String value, - @JsonProperty(access = JsonProperty.Access.READ_ONLY, defaultValue = "false") + @JsonProperty(access = JsonProperty.Access.READ_ONLY, defaultValue = "false") // Get only boolean ignored ) {} """) @@ -250,7 +336,7 @@ record Test( result == '{"other":"test","ignored":false}' when: - bean = jsonMapper.readValue(result, argumentOf(context, 'test.Test')) + bean = jsonMapper.readValue('{"other":"test","ignored":true}', argumentOf(context, 'test.Test')) then: bean.ignored == 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 8c3e16c3f..c107bece6 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 @@ -172,10 +172,6 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio for (int i = 0; i < constructorArguments.length; i++) { Argument constructorArgument = resolveArgument((Argument) constructorArguments[i]); final AnnotationMetadata annotationMetadata = resolveArgumentMetadata(introspection, constructorArgument, constructorArgument.getAnnotationMetadata()); - if (annotationMetadata.isTrue(SerdeConfig.class, SerdeConfig.IGNORED) - || annotationMetadata.isTrue(SerdeConfig.class, SerdeConfig.IGNORED_DESERIALIZATION)) { - continue; - } if (annotationMetadata.isAnnotationPresent(SerdeConfig.SerAnySetter.class)) { anySetterValue = new AnySetter<>(constructorArgument, i); continue; @@ -189,8 +185,8 @@ public DeserBean(DeserializationConfiguration defaultDeserializationConfiguratio PropertyNamingStrategy propertyNamingStrategy = getPropertyNamingStrategy(annotationMetadata, decoderContext, entityPropertyNamingStrategy); final String propertyName = resolveName(serdeArgumentConf, constructorArgument, annotationMetadata, propertyNamingStrategy); - if (allowPropertyPredicate != null && !allowPropertyPredicate.test(propertyName)) { - continue; + if (isIgnored(annotationMetadata) || (allowPropertyPredicate != null && !allowPropertyPredicate.test(propertyName))) { + ignoredProperties.add(propertyName); } Argument constructorWithPropertyArgument = Argument.of( @@ -608,8 +604,7 @@ private static Deserializer findDeserializer(Deserializer.DecoderContext return (Deserializer) decoderContext.findDeserializer(argument).createSpecific(decoderContext, argument); } - private boolean isIgnored(BeanProperty bp) { - final AnnotationMetadata annotationMetadata = bp.getAnnotationMetadata(); + private boolean isIgnored(AnnotationMetadata annotationMetadata) { return annotationMetadata.booleanValue(SerdeConfig.class, SerdeConfig.READ_ONLY).orElse(false) || annotationMetadata.booleanValue(SerdeConfig.class, SerdeConfig.IGNORED).orElse(false) || annotationMetadata.booleanValue(SerdeConfig.class, SerdeConfig.IGNORED_DESERIALIZATION).orElse(false); diff --git a/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonPropertySpec.groovy b/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonPropertySpec.groovy index 13e83fafd..285f0e563 100644 --- a/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonPropertySpec.groovy +++ b/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonPropertySpec.groovy @@ -13,6 +13,94 @@ class DatabindJsonPropertySpec extends JsonPropertySpec { )) } + @PendingFeature + void "test @JsonProperty.Access.READ_ONLY (get only) - constructor"() { + // Jackson cannot deserialize READ_ONLY as null + given: + def context = buildContext(""" +package test; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +class Test { + + private String value; + private String ignored; + + @JsonCreator + public Test(@JsonProperty("value") String value, @JsonProperty(value = "ignored", access = JsonProperty.Access.READ_ONLY) String ignored) { + this.value = value; + this.ignored = ignored; + } + + public String getValue() { + return this.value; + } + + public String getIgnored() { + return this.ignored; + } + +} +""") + when: + def bean = newInstance(context, 'test.Test', "test", "xyz") + def result = writeJson(jsonMapper, bean) + + then: + result == '{"value":"test","ignored":"xyz"}' + + when: + bean = jsonMapper.readValue('{"value":"test","ignored":"xyz"}', argumentOf(context, 'test.Test')) + + then: + bean.value == 'test' + bean.ignored == null + + cleanup: + context.close() + } + + @PendingFeature + void "test @JsonProperty.Access.READ_ONLY (get only) - record"() { + // Jackson cannot deserialize READ_ONLY as null + given: + def context = buildContext(""" +package test; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +record Test( + @JsonProperty + String value, + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + String ignored +) {} +""") + when: + def bean = newInstance(context, 'test.Test', "test", "xyz") + def result = writeJson(jsonMapper, bean) + + then: + result == '{"value":"test","ignored":"xyz"}' + + when: + bean = jsonMapper.readValue('{"value":"test","ignored":"xyz"}', argumentOf(context, 'test.Test')) + + then: + bean.value == 'test' + bean.ignored == null + + cleanup: + context.close() + } + void "test required primitive field"() { given: