Skip to content

Commit

Permalink
Support @JsonTypeInfo(include = EXISTING_PROPERTY) (#702)
Browse files Browse the repository at this point in the history
  • Loading branch information
dstepanov authored Dec 7, 2023
1 parent 7d39302 commit f4902c4
Show file tree
Hide file tree
Showing 11 changed files with 144 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@
*/
String TYPE_PROPERTY = "typeProperty";

/**
* The type discriminator type.
*/
String TYPE_DISCRIMINATOR_TYPE = "typeDiscriminatorType";

/**
* If the type property should be visible.
*/
Expand Down Expand Up @@ -337,7 +342,7 @@
* The discriminator type.
*/
enum DiscriminatorType {
PROPERTY, WRAPPER_OBJECT, WRAPPER_ARRAY
PROPERTY, WRAPPER_OBJECT, WRAPPER_ARRAY, EXISTING_PROPERTY
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)'() {
Expand Down Expand Up @@ -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"() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ protected List<AnnotationValue<?>> mapValid(AnnotationValue<Annotation> 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");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public Deserializer<Object> createSpecific(DecoderContext context, Argument<? su
subtypeDeserializers,
deserBean.ignoreUnknown
);
case PROPERTY -> new SubtypedPropertyObjectDeserializer(
case PROPERTY, EXISTING_PROPERTY -> new SubtypedPropertyObjectDeserializer(
deserBean,
subtypeDeserializers,
supertypeDeserializer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public Object deserializeNullable(@NonNull Decoder decoder, @NonNull DecoderCont
public void deserializeInto(Decoder decoder, DecoderContext decoderContext, Argument<? super Object> beanType, Object beanInstance)
throws IOException {
Decoder objectDecoder = decoder.decodeObject(beanType);
boolean completed = false;

if (properties != null) {
PropertiesBag<Object>.Consumer propertiesConsumer = properties.newConsumer();
Expand All @@ -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<Object, Object> consumedProperty = propertiesConsumer.consume(prop);
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object> {

Expand All @@ -45,8 +45,10 @@ public SubtypedPropertyObjectDeserializer(DeserBean<? super Object> 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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, Object> jsonValue;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -182,7 +188,12 @@ public int getOrder() {
}
}

Optional<String> subType = annotationMetadata.stringValue(SerdeConfig.class, SerdeConfig.TYPE_NAME);
Optional<String> subType;
if (discriminatorType != SerdeConfig.SerSubtyped.DiscriminatorType.EXISTING_PROPERTY) {
subType = annotationMetadata.stringValue(SerdeConfig.class, SerdeConfig.TYPE_NAME);
} else {
subType = Optional.empty();
}
Set<String> addedProperties = CollectionUtils.newHashSet(properties.size());

if (!properties.isEmpty() || !jsonGetters.isEmpty() || subType.isPresent()) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/docs/guide/jacksonAnnotations.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
|✅
Expand Down

0 comments on commit f4902c4

Please sign in to comment.