diff --git a/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/polymorphism/IonAnnotationStdTypeResolverBuilder.java b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/polymorphism/IonAnnotationStdTypeResolverBuilder.java new file mode 100644 index 000000000..0b8e7a663 --- /dev/null +++ b/ion/src/main/java/com/fasterxml/jackson/dataformat/ion/polymorphism/IonAnnotationStdTypeResolverBuilder.java @@ -0,0 +1,61 @@ +package com.fasterxml.jackson.dataformat.ion.polymorphism; + +import java.util.Collection; + +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder; + +/** + * A replacement for {@link IonAnnotationTypeResolverBuilder} which uses Jackson {@link StdTypeResolverBuilder}. This + * allows for configuration via standard Jackson type annotations and removes the need for using + * {@link IonAnnotationIntrospector} and hence no additional Module. See {@link JsonTypeInfoAnnotationsTest} for example + * usage. + */ +public class IonAnnotationStdTypeResolverBuilder extends StdTypeResolverBuilder { + + @Override + public TypeSerializer buildTypeSerializer( + SerializationConfig config, + JavaType baseType, + Collection subtypes) { + return new IonAnnotationTypeSerializer( + idResolver( + config, + baseType, + subtypes, + true, // Indicates the id resolver is for serialization + false)); // Indicates the id resolver is not for deserialization + } + + @Override + public TypeDeserializer buildTypeDeserializer( + DeserializationConfig config, + JavaType baseType, + Collection subtypes) { + + final JavaType defaultImpl; + if (_defaultImpl == null) { + defaultImpl = null; + } else { + defaultImpl = config.getTypeFactory() .constructSpecializedType(baseType, _defaultImpl); + } + + return new IonAnnotationTypeDeserializer( + baseType, + idResolver( + config, + baseType, + subtypes, + false, // Indicates the id resolver is not for serialization + true), // Indicates the id resolver is for deserialization + _typeProperty, + _typeIdVisible, + defaultImpl); + } + +} diff --git a/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/polymorphism/JsonTypeInfoAnnotationsTest.java b/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/polymorphism/JsonTypeInfoAnnotationsTest.java new file mode 100644 index 000000000..1d678f0e6 --- /dev/null +++ b/ion/src/test/java/com/fasterxml/jackson/dataformat/ion/polymorphism/JsonTypeInfoAnnotationsTest.java @@ -0,0 +1,236 @@ +package com.fasterxml.jackson.dataformat.ion.polymorphism; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import software.amazon.ion.IonSystem; +import software.amazon.ion.IonValue; +import software.amazon.ion.system.IonSystemBuilder; +import com.fasterxml.jackson.dataformat.ion.IonFactory; +import com.fasterxml.jackson.dataformat.ion.IonObjectMapper; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonTypeResolver; + +public class JsonTypeInfoAnnotationsTest { + + @Test + public void testSimple() throws IOException { + + IonSystem ios = IonSystemBuilder.standard().build(); + ObjectMapper mapper = new IonObjectMapper(new IonFactory(null, ios)); + Map mapField = new HashMap<>(); + mapField.put("key1", 1); + mapField.put("key2", 2); + + Bean original = new Bean( + "parent", + new ChildBean("foo"), + new ChildBeanSub("foo", "bar"), + new ChildBeanValueTypeSub("fiz"), + new DefaultChild("baz"), + new AnotherChild("anotherBaz"), + new MapValueTypeChild(mapField), + new ListValueTypeChild(Arrays.asList(1, 2, 3))); + + IonValue expected = ios.singleValue( + "{" + + " field:\"parent\"," + + " childBean:ChildA::{" + + " someField:\"foo\"" + + " }," + + " childSub:ChildB::{" + + " someField:\"foo\"," + + " extraField:\"bar\"" + + " }," + + " childValueTypeSub:ChildC::\"fiz\"," + + " defaultChild:'.JsonTypeInfoAnnotationsTest$DefaultChild'::{" + + " thisIsAField:\"baz\"" + + " }," + + " anotherChild:'.JsonTypeInfoAnnotationsTest$AnotherChild'::{" + + " thisIsAnotherField:\"anotherBaz\"" + + " }," + + " mapValueTypeChild: '.JsonTypeInfoAnnotationsTest$MapValueTypeChild'::{ " + + " key1: 1," + + " key2: 2," + + " }," + + " listValueTypeChild: '.JsonTypeInfoAnnotationsTest$ListValueTypeChild'::[1, 2, 3]" + + "}"); + + String serialized = mapper.writeValueAsString(original); + + Assert.assertEquals(expected, ios.singleValue(serialized)); + + // Make sure it can be re-serialized + mapper.readValue(serialized, Bean.class); + } + + @Test + public void testDefaultType() throws IOException { + + IonSystem ios = IonSystemBuilder.standard().build(); + ObjectMapper mapper = new IonObjectMapper(new IonFactory(null, ios)); + + final String unTypedIon = + "{" + + " defaultChild:{" + + " thisIsAField:\"test\"" + + " }" + + "}"; + + DefaultBean bean = mapper.readValue(unTypedIon, DefaultBean.class); + + Assert.assertSame(DefaultChild.class, bean.defaultChild.getClass()); + } + + static class Bean { + public String field; + public ChildBean childBean; + public ChildBean childSub; + public ChildBean childValueTypeSub; + public ChildInterface defaultChild; + public ChildInterface anotherChild; + public ChildInterface mapValueTypeChild; + public ChildInterface listValueTypeChild; + + public Bean() { + } + + public Bean( + String field, + ChildBean childBean, + ChildBean childSub, + ChildBean childValueTypeSub, + ChildInterface defaultChild, + ChildInterface anotherChild, + ChildInterface mapValueTypeChild, + ChildInterface listValueTypeChild) { + this.field = field; + this.childBean = childBean; + this.childSub = childSub; + this.childValueTypeSub = childValueTypeSub; + this.defaultChild = defaultChild; + this.anotherChild = anotherChild; + this.mapValueTypeChild = mapValueTypeChild; + this.listValueTypeChild = listValueTypeChild; + } + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME) + @JsonTypeResolver(IonAnnotationStdTypeResolverBuilder.class) + @JsonTypeName("ChildA") + @JsonSubTypes({ + @Type(ChildBeanSub.class), + @Type(ChildBeanValueTypeSub.class) + }) + static class ChildBean { + public String someField; + + public ChildBean() { + } + + public ChildBean(String someField) { + this.someField = someField; + } + } + + @JsonTypeName("ChildB") + static class ChildBeanSub extends ChildBean { + public String extraField; + + public ChildBeanSub() { + } + + public ChildBeanSub(String someField, String extraField) { + super(someField); + this.extraField = extraField; + } + } + + @JsonTypeName("ChildC") + static class ChildBeanValueTypeSub extends ChildBean { + + @JsonCreator + public ChildBeanValueTypeSub(String someField) { + super(someField); + } + + @JsonValue + public String getSomeField() { + return someField; + } + } + + @JsonTypeInfo( + use = JsonTypeInfo.Id.MINIMAL_CLASS, + defaultImpl = DefaultChild.class) + @JsonTypeResolver(IonAnnotationStdTypeResolverBuilder.class) + static interface ChildInterface { + } + + static class DefaultChild implements ChildInterface { + public String thisIsAField; + + public DefaultChild() { + } + + public DefaultChild(String someField) { + this.thisIsAField = someField; + } + } + + static class AnotherChild implements ChildInterface { + public String thisIsAnotherField; + + public AnotherChild() { + } + + public AnotherChild(String someField) { + this.thisIsAnotherField = someField; + } + } + + static class MapValueTypeChild implements ChildInterface { + public Map mapField; + + @JsonCreator + public MapValueTypeChild(Map mapField) { + this.mapField = mapField; + } + + @JsonValue + public Map getMapField() { + return mapField; + } + } + + static class ListValueTypeChild implements ChildInterface { + public List listField; + + @JsonCreator + public ListValueTypeChild(List listField) { + this.listField = listField; + } + + @JsonValue + public List getMapField() { + return listField; + } + } + + static final class DefaultBean { + public ChildInterface defaultChild; + } + +}