diff --git a/serde-api/src/main/java/io/micronaut/serde/config/annotation/SerdeConfig.java b/serde-api/src/main/java/io/micronaut/serde/config/annotation/SerdeConfig.java index 542540a1b..7fc3fa6b2 100644 --- a/serde-api/src/main/java/io/micronaut/serde/config/annotation/SerdeConfig.java +++ b/serde-api/src/main/java/io/micronaut/serde/config/annotation/SerdeConfig.java @@ -129,6 +129,11 @@ */ String TYPE_PROPERTY = "typeProperty"; + /** + * The type discriminator type. + */ + String TYPE_DISCRIMINATOR_TYPE = "typeDiscriminatorType"; + /** * If the type property should be visible. */ @@ -337,7 +342,7 @@ * The discriminator type. */ enum DiscriminatorType { - PROPERTY, WRAPPER_OBJECT, WRAPPER_ARRAY + PROPERTY, WRAPPER_OBJECT, WRAPPER_ARRAY, EXISTING_PROPERTY } /** diff --git a/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonTypeInfoSpec.groovy b/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonTypeInfoSpec.groovy index 8f14425da..7de31e78d 100644 --- a/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonTypeInfoSpec.groovy +++ b/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonTypeInfoSpec.groovy @@ -348,7 +348,7 @@ class B extends Base { compiled.close() } - def 'test @JsonTypeInfo with property'() { + def 'test @JsonTypeInfo with include = JsonTypeInfo.As.PROPERTY'() { given: def compiled = buildContext('example.Base', ''' package example; @@ -388,6 +388,93 @@ class B extends Base { compiled.close() } + def 'test @JsonTypeInfo with include = JsonTypeInfo.As.EXISTING_PROPERTY'() { + when: + def compiled = buildContext('example.Base', ''' +package example; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; + +@Introspected(accessKind = Introspected.AccessKind.FIELD) +@JsonSubTypes({ + @JsonSubTypes.Type(value = A.class, name = "a"), + @JsonSubTypes.Type(value = B.class, names = {"b", "c"}) +}) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type") +class Base { + public String type; +} + +class A extends Base { + public String fieldA; +} +class B extends Base { + public String fieldB; +} +''', true) + def baseClass = compiled.classLoader.loadClass('example.Base') + def a = newInstance(compiled, 'example.A') + a.fieldA = 'foo' + a.type = "xyz" + + then: + serializeToString(jsonMapper, a) == '{"type":"xyz","fieldA":"foo"}' + deserializeFromString(jsonMapper, baseClass, '{"type":"a","fieldA":"foo"}').fieldA == 'foo' + deserializeFromString(jsonMapper, baseClass, '{"type":"b","fieldB":"foo"}').fieldB == 'foo' + deserializeFromString(jsonMapper, baseClass, '{"type":"c","fieldB":"foo"}').fieldB == 'foo' + deserializeFromString(jsonMapper, baseClass, '{"type":"c","fieldB":"foo"}').type == null + + cleanup: + compiled.close() + } + + def 'test @JsonTypeInfo with include = JsonTypeInfo.As.EXISTING_PROPERTY and visible = true'() { + when: + def compiled = buildContext('example.Base', ''' +package example; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; + +@Introspected(accessKind = Introspected.AccessKind.FIELD) +@JsonSubTypes({ + @JsonSubTypes.Type(value = A.class, name = "a"), + @JsonSubTypes.Type(value = B.class, names = {"b", "c"}) +}) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true) +class Base { + public String type; +} + +class A extends Base { + public String fieldA; +} +class B extends Base { + public String fieldB; +} +''', true) + def baseClass = compiled.classLoader.loadClass('example.Base') + def a = newInstance(compiled, 'example.A') + a.fieldA = 'foo' + a.type = "xyz" + + then: + serializeToString(jsonMapper, a) == '{"type":"xyz","fieldA":"foo"}' + deserializeFromString(jsonMapper, baseClass, '{"type":"a","fieldA":"foo"}').fieldA == 'foo' + deserializeFromString(jsonMapper, baseClass, '{"type":"a","fieldA":"foo"}').type == 'a' + deserializeFromString(jsonMapper, baseClass, '{"type":"b","fieldB":"foo"}').fieldB == 'foo' + deserializeFromString(jsonMapper, baseClass, '{"type":"c","fieldB":"foo"}').fieldB == 'foo' + deserializeFromString(jsonMapper, baseClass, '{"type":"c","fieldB":"foo"}').type == "c" + + cleanup: + compiled.close() + } + void "test find type info in record interface"() { given: def context = buildContext(""" diff --git a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonTypeInfoSpec.groovy b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonTypeInfoSpec.groovy index a6f40e964..8e45f730f 100644 --- a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonTypeInfoSpec.groovy +++ b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonTypeInfoSpec.groovy @@ -45,11 +45,12 @@ class Wrapper { } @Serdeable -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.$include) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.$include, property = "type") @JsonSubTypes( @JsonSubTypes.Type(value = Sub.class, name = "sub-class") ) class Base { + private String type; private String string; public Base(String string) { @@ -59,6 +60,14 @@ class Base { public String getString() { return string; } + + public void setType(String type) { + this.type = type; + } + + public String getType() { + return type; + } } @Serdeable @@ -77,6 +86,9 @@ class Sub extends Base { """) when: def base = newInstance(context, 'test.Sub', "a", 1) + if (include == "EXISTING_PROPERTY") { + base.type = "sub-class" + } def wrapper = newInstance(context, 'test.Wrapper', "bar", base) def result = writeJson(jsonMapper, wrapper) @@ -92,7 +104,7 @@ class Sub extends Base { context.close() where: - include << ["WRAPPER_OBJECT", "PROPERTY"] + include << ["WRAPPER_OBJECT", "PROPERTY", "EXISTING_PROPERTY"] } void 'test wrapped subtype with @JsonTypeInfo(include = WRAPPER_ARRAY)'() { @@ -419,10 +431,10 @@ class Test { """) then: def e = thrown(RuntimeException) - e.message.contains("Only 'include' of type PROPERTY, WRAPPER_OBJECT or WRAPPER_ARRAY are supported") + e.message.contains("Only 'include' of type PROPERTY, EXISTING_PROPERTY, WRAPPER_OBJECT or WRAPPER_ARRAY are supported") where: - include << JsonTypeInfo.As.values() - [JsonTypeInfo.As.PROPERTY, JsonTypeInfo.As.WRAPPER_OBJECT, JsonTypeInfo.As.WRAPPER_ARRAY] + include << JsonTypeInfo.As.values() - [JsonTypeInfo.As.PROPERTY, JsonTypeInfo.As.EXISTING_PROPERTY, JsonTypeInfo.As.WRAPPER_OBJECT, JsonTypeInfo.As.WRAPPER_ARRAY] } void "test default implementation - with @DefaultImplementation"() { diff --git a/serde-processor/src/main/java/io/micronaut/serde/processor/SerdeAnnotationVisitor.java b/serde-processor/src/main/java/io/micronaut/serde/processor/SerdeAnnotationVisitor.java index 1c80cf557..9feac5a98 100644 --- a/serde-processor/src/main/java/io/micronaut/serde/processor/SerdeAnnotationVisitor.java +++ b/serde-processor/src/main/java/io/micronaut/serde/processor/SerdeAnnotationVisitor.java @@ -673,7 +673,9 @@ private void visitSubtype(ClassElement supertype, ClassElement subtype, VisitorC switch (discriminatorType) { case WRAPPER_OBJECT -> builder.member(SerdeConfig.WRAPPER_PROPERTY, allNames.get(0)); case WRAPPER_ARRAY -> builder.member(SerdeConfig.ARRAY_WRAPPER_PROPERTY, allNames.get(0)); - default -> builder.member(SerdeConfig.TYPE_PROPERTY, typeProperty); + case PROPERTY -> builder.member(SerdeConfig.TYPE_PROPERTY, typeProperty); + case EXISTING_PROPERTY -> builder.member(SerdeConfig.TYPE_DISCRIMINATOR_TYPE, discriminatorType); + default -> throw new IllegalStateException("Unknown " + discriminatorType); } if (supertype.booleanValue(SerdeConfig.SerSubtyped.class, SerdeConfig.SerSubtyped.DISCRIMINATOR_VISIBLE).orElse(false)) { diff --git a/serde-processor/src/main/java/io/micronaut/serde/processor/jackson/JsonTypeInfoMapper.java b/serde-processor/src/main/java/io/micronaut/serde/processor/jackson/JsonTypeInfoMapper.java index 1e8e65e8c..8d8c0efeb 100644 --- a/serde-processor/src/main/java/io/micronaut/serde/processor/jackson/JsonTypeInfoMapper.java +++ b/serde-processor/src/main/java/io/micronaut/serde/processor/jackson/JsonTypeInfoMapper.java @@ -71,9 +71,9 @@ protected List> mapValid(AnnotationValue annotati ); String include = annotation.stringValue("include").orElse("PROPERTY"); switch (include) { - case "PROPERTY", "WRAPPER_OBJECT", "WRAPPER_ARRAY" -> builder.member(SerdeConfig.SerSubtyped.DISCRIMINATOR_TYPE, include); + case "PROPERTY", "WRAPPER_OBJECT", "WRAPPER_ARRAY", "EXISTING_PROPERTY" -> builder.member(SerdeConfig.SerSubtyped.DISCRIMINATOR_TYPE, include); default -> { - return mapError("Only 'include' of type PROPERTY, WRAPPER_OBJECT or WRAPPER_ARRAY are supported"); + return mapError("Only 'include' of type PROPERTY, EXISTING_PROPERTY, WRAPPER_OBJECT or WRAPPER_ARRAY are supported"); } } diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/ObjectDeserializer.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/ObjectDeserializer.java index 2c8215733..92e885c41 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/ObjectDeserializer.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/ObjectDeserializer.java @@ -86,7 +86,7 @@ public Deserializer createSpecific(DecoderContext context, Argument new SubtypedPropertyObjectDeserializer( + case PROPERTY, EXISTING_PROPERTY -> new SubtypedPropertyObjectDeserializer( deserBean, subtypeDeserializers, supertypeDeserializer, diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SimpleObjectDeserializer.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SimpleObjectDeserializer.java index 80d2497a8..b75bb0cd5 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SimpleObjectDeserializer.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SimpleObjectDeserializer.java @@ -81,6 +81,7 @@ public Object deserializeNullable(@NonNull Decoder decoder, @NonNull DecoderCont public void deserializeInto(Decoder decoder, DecoderContext decoderContext, Argument beanType, Object beanInstance) throws IOException { Decoder objectDecoder = decoder.decodeObject(beanType); + boolean completed = false; if (properties != null) { PropertiesBag.Consumer propertiesConsumer = properties.newConsumer(); @@ -89,6 +90,7 @@ public void deserializeInto(Decoder decoder, DecoderContext decoderContext, Argu while (!allConsumed) { final String prop = objectDecoder.decodeKey(); if (prop == null) { + completed = true; break; } final DeserBean.DerProperty consumedProperty = propertiesConsumer.consume(prop); @@ -110,7 +112,9 @@ public void deserializeInto(Decoder decoder, DecoderContext decoderContext, Argu } } - if (ignoreUnknown) { + if (completed) { + objectDecoder.finishStructure(); + } else if (ignoreUnknown) { objectDecoder.finishStructure(true); } else { String unknownProp = objectDecoder.decodeKey(); diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SpecificObjectDeserializer.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SpecificObjectDeserializer.java index 675d911f3..d2b444916 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SpecificObjectDeserializer.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SpecificObjectDeserializer.java @@ -144,7 +144,9 @@ private static BeanDeserializer newBeanDeserializer(Object instance, return new BuilderDeserializer(db, conf); } if (allowSubtype && db.subtypeInfo != null) { - if (db.subtypeInfo.discriminatorType() == SerdeConfig.SerSubtyped.DiscriminatorType.PROPERTY) { + SerdeConfig.SerSubtyped.DiscriminatorType discriminatorType = db.subtypeInfo.discriminatorType(); + if (discriminatorType == SerdeConfig.SerSubtyped.DiscriminatorType.PROPERTY + || discriminatorType == SerdeConfig.SerSubtyped.DiscriminatorType.EXISTING_PROPERTY) { return new SubtypedPropertyBeanDeserializer(db, argument, conf); } return new SubtypedWrapperBeanDeserializer(db, argument, conf); diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SubtypedPropertyObjectDeserializer.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SubtypedPropertyObjectDeserializer.java index 936906cf0..380ca217f 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SubtypedPropertyObjectDeserializer.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/SubtypedPropertyObjectDeserializer.java @@ -25,10 +25,10 @@ import java.util.Map; /** - * Implementation for deserialization of objects that uses introspection metadata. + * Subtyped property deserializer. * - * @author graemerocher - * @since 1.0.0 + * @author Denis Stepanov + * @since 2.4.0 */ final class SubtypedPropertyObjectDeserializer implements Deserializer { @@ -45,8 +45,10 @@ public SubtypedPropertyObjectDeserializer(DeserBean deserBean, this.deserializers = deserializers; this.supertypeDeserializer = supertypeDeserializer; this.discriminatorVisible = discriminatorVisible; - if (deserBean.subtypeInfo.discriminatorType() != SerdeConfig.SerSubtyped.DiscriminatorType.PROPERTY) { - throw new IllegalStateException("Unsupported discriminator type: " + deserBean.subtypeInfo.discriminatorType()); + SerdeConfig.SerSubtyped.DiscriminatorType discriminatorType = deserBean.subtypeInfo.discriminatorType(); + if (discriminatorType != SerdeConfig.SerSubtyped.DiscriminatorType.PROPERTY + && discriminatorType != SerdeConfig.SerSubtyped.DiscriminatorType.EXISTING_PROPERTY) { + throw new IllegalStateException("Unsupported discriminator type: " + discriminatorType); } } diff --git a/serde-support/src/main/java/io/micronaut/serde/support/serializers/SerBean.java b/serde-support/src/main/java/io/micronaut/serde/support/serializers/SerBean.java index cd82cbbf0..f7ade9b8a 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/serializers/SerBean.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/serializers/SerBean.java @@ -84,6 +84,7 @@ public int getOrder() { @Nullable public final String wrapperProperty; @Nullable + public final SerdeConfig.SerSubtyped.DiscriminatorType discriminatorType; public final String arrayWrapperProperty; @Nullable public SerProperty jsonValue; @@ -112,6 +113,11 @@ public int getOrder() { this.configuration = configuration; this.introspection = introspections.getSerializableIntrospection(type); this.propertyFilter = getPropertyFilterIfPresent(beanContext, type.getSimpleName()); + this.discriminatorType = introspection.enumValue( + SerdeConfig.class, + SerdeConfig.TYPE_DISCRIMINATOR_TYPE, + SerdeConfig.SerSubtyped.DiscriminatorType.class + ).orElse(null); boolean allowIgnoredProperties = introspection.booleanValue(SerdeConfig.SerIgnored.class, SerdeConfig.SerIgnored.ALLOW_SERIALIZE).orElse(false); @@ -182,7 +188,12 @@ public int getOrder() { } } - Optional subType = annotationMetadata.stringValue(SerdeConfig.class, SerdeConfig.TYPE_NAME); + Optional subType; + if (discriminatorType != SerdeConfig.SerSubtyped.DiscriminatorType.EXISTING_PROPERTY) { + subType = annotationMetadata.stringValue(SerdeConfig.class, SerdeConfig.TYPE_NAME); + } else { + subType = Optional.empty(); + } Set addedProperties = CollectionUtils.newHashSet(properties.size()); if (!properties.isEmpty() || !jsonGetters.isEmpty() || subType.isPresent()) { diff --git a/src/main/docs/guide/jacksonAnnotations.adoc b/src/main/docs/guide/jacksonAnnotations.adoc index 528dd4322..de972080f 100644 --- a/src/main/docs/guide/jacksonAnnotations.adoc +++ b/src/main/docs/guide/jacksonAnnotations.adoc @@ -129,7 +129,7 @@ NOTE: If an unsupported annotation or member is used, a compilation error will r |link:{jacksonAnnotationJavadoc}/JsonTypeInfo.html[@JsonTypeInfo] |✅ -|Only `WRAPPER_OBJECT`, `WRAPPER_ARRAY` & `PROPERTY` for `include` and only `CLASS` & `NAME` for `use`. +|Only `WRAPPER_OBJECT`, `WRAPPER_ARRAY`, `PROPERTY`, `EXISTING_PROPERTY` for `include` and only `CLASS` & `NAME` for `use`. |link:{jacksonAnnotationJavadoc}/JsonTypeName.html[@JsonTypeName] |✅