diff --git a/src/main/java/tools/jackson/databind/introspect/JacksonAnnotationIntrospector.java b/src/main/java/tools/jackson/databind/introspect/JacksonAnnotationIntrospector.java index 43d67300ad..504bdff107 100644 --- a/src/main/java/tools/jackson/databind/introspect/JacksonAnnotationIntrospector.java +++ b/src/main/java/tools/jackson/databind/introspect/JacksonAnnotationIntrospector.java @@ -651,13 +651,27 @@ public TypeResolverBuilder findPropertyContentTypeResolver(MapperConfig co public List findSubtypes(MapperConfig config, Annotated a) { JsonSubTypes t = _findAnnotation(a, JsonSubTypes.class); - if (t == null) return null; + if(t != null) { + return findSubtypesByJsonSubTypesAnnotation(config, a, t); + } + + if(a.getAnnotated() instanceof Class clazz && clazz.isSealed() + && clazz.getPermittedSubclasses().length > 0) { + return findSubtypesByPermittedSubclasses(config, a, clazz); + } + + return null; + } + + // @since 3.0 + private List findSubtypesByJsonSubTypesAnnotation(MapperConfig config, Annotated a, JsonSubTypes t) + { JsonSubTypes.Type[] types = t.value(); // 02-Aug-2022, tatu: As per [databind#3500], may need to check uniqueness // of names if (t.failOnRepeatedNames()) { - return findSubtypesCheckRepeatedNames(a.getName(), types); + return findSubtypesByJsonSubTypesAnnotationCheckRepeatedNames(a.getName(), types); } else { ArrayList result = new ArrayList(types.length); for (JsonSubTypes.Type type : types) { @@ -671,8 +685,8 @@ public List findSubtypes(MapperConfig config, Annotated a) } } - // @since 2.14 - private List findSubtypesCheckRepeatedNames(String annotatedTypeName, JsonSubTypes.Type[] types) + // @since 3.0 + private List findSubtypesByJsonSubTypesAnnotationCheckRepeatedNames(String annotatedTypeName, JsonSubTypes.Type[] types) { ArrayList result = new ArrayList(types.length); Set seenNames = new HashSet<>(); @@ -698,6 +712,16 @@ private List findSubtypesCheckRepeatedNames(String annotatedTypeName, return result; } + + // @since 3.0 + private List findSubtypesByPermittedSubclasses(MapperConfig config, Annotated a, Class clazz) + { + List result = new ArrayList<>(clazz.getPermittedSubclasses().length); + for (Class subtype : clazz.getPermittedSubclasses()) { + result.add(new NamedType(subtype)); + } + return result; + } @Override public String findTypeName(MapperConfig config, AnnotatedClass ac) diff --git a/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithExistingPropertyTest.java b/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithExistingPropertyTest.java new file mode 100644 index 0000000000..1dccc69233 --- /dev/null +++ b/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithExistingPropertyTest.java @@ -0,0 +1,546 @@ +package tools.jackson.databind.jsontype; + +import java.util.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +public class SealedTypesWithExistingPropertyTest extends DatabindTestUtil +{ + /** + * Polymorphic base class - existing property as simple property on subclasses + */ + @JsonTypeInfo(use = Id.NAME, include = As.EXISTING_PROPERTY, property = "type", + visible=true) + static abstract sealed class Fruit permits Apple, Orange { + public String name; + protected Fruit(String n) { name = n; } + } + + @JsonTypeName("apple") + @JsonPropertyOrder({ "name", "seedCount", "type" }) + static final class Apple extends Fruit + { + public int seedCount; + public String type; + + private Apple() { super(null); } + public Apple(String name, int b) { + super(name); + seedCount = b; + type = "apple"; + } + } + + @JsonTypeName("orange") + @JsonPropertyOrder({ "name", "color", "type" }) + static final class Orange extends Fruit + { + public String color; + public String type; + + private Orange() { super(null); } + public Orange(String name, String c) { + super(name); + color = c; + type = "orange"; + } + } + + static class FruitWrapper { + public Fruit fruit; + public FruitWrapper() {} + public FruitWrapper(Fruit f) { fruit = f; } + } + + /** + * Polymorphic base class - existing property forced by abstract method + */ + @JsonTypeInfo(use = Id.NAME, include = As.EXISTING_PROPERTY, property = "type") + static abstract sealed class Animal permits Dog, Cat { + public String name; + + protected Animal(String n) { name = n; } + + public abstract String getType(); + } + + @JsonTypeName("doggie") + static final class Dog extends Animal + { + public int boneCount; + + private Dog() { super(null); } + public Dog(String name, int b) { + super(name); + boneCount = b; + } + + @Override + public String getType() { + return "doggie"; + } + } + + @JsonTypeName("kitty") + static final class Cat extends Animal + { + public String furColor; + + private Cat() { super(null); } + public Cat(String name, String c) { + super(name); + furColor = c; + } + + @Override + public String getType() { + return "kitty"; + } + } + + static class AnimalWrapper { + public Animal animal; + public AnimalWrapper() {} + public AnimalWrapper(Animal a) { animal = a; } + } + + /** + * Polymorphic base class - existing property NOT forced by abstract method on base class + */ + @JsonTypeInfo(use = Id.NAME, include = As.EXISTING_PROPERTY, property = "type") + static abstract sealed class Car permits Accord, Camry { + public String name; + protected Car(String n) { name = n; } + } + + @JsonTypeName("accord") + static final class Accord extends Car + { + public int speakerCount; + + private Accord() { super(null); } + public Accord(String name, int b) { + super(name); + speakerCount = b; + } + + public String getType() { + return "accord"; + } + } + + @JsonTypeName("camry") + static final class Camry extends Car + { + public String exteriorColor; + + private Camry() { super(null); } + public Camry(String name, String c) { + super(name); + exteriorColor = c; + } + + public String getType() { + return "camry"; + } + } + + static class CarWrapper { + public Car car; + public CarWrapper() {} + public CarWrapper(Car c) { car = c; } + } + + // for [databind#1635] + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + // IMPORTANT! Must be defined as `visible` + visible=true, + property = "type", + defaultImpl=Bean1635Default.class) + @JsonSubTypes({ @JsonSubTypes.Type(Bean1635A.class) }) + static class Bean1635 { + public ABC type; + } + + @JsonTypeName("A") + static class Bean1635A extends Bean1635 { + public int value; + } + + static class Bean1635Default extends Bean1635 { } + + // [databind#3251]: Double vs BigDecimal + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "type_alias" + ) + static class GenericWrapperWithNew3251 { + private final T value; + + @JsonCreator + public GenericWrapperWithNew3251(@JsonProperty("value") T value) { + this.value = value; + } + + public T getValue() { + return value; + } + } + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "fieldType", + visible = true, + defaultImpl = GenericWrapperWithExisting3251.class + ) + static class GenericWrapperWithExisting3251 { + public String fieldType; + private final T value; + + @JsonCreator + public GenericWrapperWithExisting3251(@JsonProperty("value") T value) { + this.value = value; + } + + public T getValue() { + return value; + } + } + + // [databind#3271] + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, + visible = true, property = "type", defaultImpl = DefaultShape3271.class) + @JsonSubTypes({@JsonSubTypes.Type(value = Square3271.class, name = "square")}) + static abstract class Shape3271 { + public String type; + + public String getType() { return this.type; } + + public void setType(String type) { this.type = type; } + } + + static class Square3271 extends Shape3271 {} + + static class DefaultShape3271 extends Shape3271 {} + + /* + /********************************************************************** + /* Mock data + /********************************************************************** + */ + + private static final Orange mandarin = new Orange("Mandarin Orange", "orange"); + private static final String mandarinJson = "{\"name\":\"Mandarin Orange\",\"color\":\"orange\",\"type\":\"orange\"}"; + private static final Apple pinguo = new Apple("Apple-A-Day", 16); + private static final String pinguoJson = "{\"name\":\"Apple-A-Day\",\"seedCount\":16,\"type\":\"apple\"}"; + private static final FruitWrapper pinguoWrapper = new FruitWrapper(pinguo); + private static final String pinguoWrapperJson = "{\"fruit\":" + pinguoJson + "}"; + private static final List fruitList = Arrays.asList(pinguo, mandarin); + private static final String fruitListJson = "[" + pinguoJson + "," + mandarinJson + "]"; + + private static final Cat beelzebub = new Cat("Beelzebub", "tabby"); + private static final String beelzebubJson = "{\"furColor\":\"tabby\",\"name\":\"Beelzebub\",\"type\":\"kitty\"}"; + private static final Dog rover = new Dog("Rover", 42); + private static final String roverJson = "{\"boneCount\":42,\"name\":\"Rover\",\"type\":\"doggie\"}"; + private static final AnimalWrapper beelzebubWrapper = new AnimalWrapper(beelzebub); + private static final String beelzebubWrapperJson = "{\"animal\":" + beelzebubJson + "}"; + private static final List animalList = Arrays.asList(beelzebub, rover); + private static final String animalListJson = "[" + beelzebubJson + "," + roverJson + "]"; + + private static final Camry camry = new Camry("Sweet Ride", "candy-apple-red"); + private static final String camryJson = "{\"exteriorColor\":\"candy-apple-red\",\"name\":\"Sweet Ride\",\"type\":\"camry\"}"; + private static final Accord accord = new Accord("Road Rage", 6); + private static final String accordJson = "{\"name\":\"Road Rage\",\"speakerCount\":6,\"type\":\"accord\"}"; + private static final CarWrapper camryWrapper = new CarWrapper(camry); + private static final String camryWrapperJson = "{\"car\":" + camryJson + "}"; + private static final List carList = Arrays.asList(camry, accord); + private static final String carListJson = "[" + camryJson + "," + accordJson + "]"; + + /* + /********************************************************************** + /* Test methods + /********************************************************************** + */ + + private final ObjectMapper MAPPER = newJsonMapper(); + + /** + * Fruits - serialization tests for simple property on sub-classes + */ + @Test + public void testExistingPropertySerializationFruits() throws Exception + { + Map result = writeAndMap(MAPPER, pinguo); + assertEquals(3, result.size()); + assertEquals(pinguo.name, result.get("name")); + assertEquals(pinguo.seedCount, result.get("seedCount")); + assertEquals(pinguo.type, result.get("type")); + + result = writeAndMap(MAPPER, mandarin); + assertEquals(3, result.size()); + assertEquals(mandarin.name, result.get("name")); + assertEquals(mandarin.color, result.get("color")); + assertEquals(mandarin.type, result.get("type")); + + String pinguoSerialized = MAPPER.writeValueAsString(pinguo); + assertEquals(pinguoSerialized, pinguoJson); + + String mandarinSerialized = MAPPER.writeValueAsString(mandarin); + assertEquals(mandarinSerialized, mandarinJson); + + String fruitWrapperSerialized = MAPPER.writeValueAsString(pinguoWrapper); + assertEquals(fruitWrapperSerialized, pinguoWrapperJson); + + String fruitListSerialized = MAPPER.writeValueAsString(fruitList); + assertEquals(fruitListSerialized, fruitListJson); + } + + /** + * Fruits - deserialization tests for simple property on sub-classes + */ + @Test + public void testSimpleClassAsExistingPropertyDeserializationFruits() throws Exception + { + Fruit pinguoDeserialized = MAPPER.readValue(pinguoJson, Fruit.class); + assertTrue(pinguoDeserialized instanceof Apple); + assertSame(pinguoDeserialized.getClass(), Apple.class); + assertEquals(pinguo.name, pinguoDeserialized.name); + assertEquals(pinguo.seedCount, ((Apple) pinguoDeserialized).seedCount); + assertEquals(pinguo.type, ((Apple) pinguoDeserialized).type); + + FruitWrapper pinguoWrapperDeserialized = MAPPER.readValue(pinguoWrapperJson, FruitWrapper.class); + Fruit pinguoExtracted = pinguoWrapperDeserialized.fruit; + assertTrue(pinguoExtracted instanceof Apple); + assertSame(pinguoExtracted.getClass(), Apple.class); + assertEquals(pinguo.name, pinguoExtracted.name); + assertEquals(pinguo.seedCount, ((Apple) pinguoExtracted).seedCount); + assertEquals(pinguo.type, ((Apple) pinguoExtracted).type); + + Fruit[] fruits = MAPPER.readValue(fruitListJson, Fruit[].class); + assertEquals(2, fruits.length); + assertEquals(Apple.class, fruits[0].getClass()); + assertEquals("apple", ((Apple) fruits[0]).type); + assertEquals(Orange.class, fruits[1].getClass()); + assertEquals("orange", ((Orange) fruits[1]).type); + + List f2 = MAPPER.readValue(fruitListJson, + new TypeReference>() { }); + assertNotNull(f2); + assertTrue(f2.size() == 2); + assertEquals(Apple.class, f2.get(0).getClass()); + assertEquals(Orange.class, f2.get(1).getClass()); + } + + /** + * Animals - serialization tests for abstract method in base class + */ + @Test + public void testExistingPropertySerializationAnimals() throws Exception + { + Map result = writeAndMap(MAPPER, beelzebub); + assertEquals(3, result.size()); + assertEquals(beelzebub.name, result.get("name")); + assertEquals(beelzebub.furColor, result.get("furColor")); + assertEquals(beelzebub.getType(), result.get("type")); + + result = writeAndMap(MAPPER, rover); + assertEquals(3, result.size()); + assertEquals(rover.name, result.get("name")); + assertEquals(rover.boneCount, result.get("boneCount")); + assertEquals(rover.getType(), result.get("type")); + + String beelzebubSerialized = MAPPER.writeValueAsString(beelzebub); + assertEquals(beelzebubSerialized, beelzebubJson); + + String roverSerialized = MAPPER.writeValueAsString(rover); + assertEquals(roverSerialized, roverJson); + + String animalWrapperSerialized = MAPPER.writeValueAsString(beelzebubWrapper); + assertEquals(animalWrapperSerialized, beelzebubWrapperJson); + + String animalListSerialized = MAPPER.writeValueAsString(animalList); + assertEquals(animalListSerialized, animalListJson); + } + + /** + * Animals - deserialization tests for abstract method in base class + */ + @Test + public void testSimpleClassAsExistingPropertyDeserializationAnimals() throws Exception + { + Animal beelzebubDeserialized = MAPPER.readValue(beelzebubJson, Animal.class); + assertTrue(beelzebubDeserialized instanceof Cat); + assertSame(beelzebubDeserialized.getClass(), Cat.class); + assertEquals(beelzebub.name, beelzebubDeserialized.name); + assertEquals(beelzebub.furColor, ((Cat) beelzebubDeserialized).furColor); + assertEquals(beelzebub.getType(), beelzebubDeserialized.getType()); + + AnimalWrapper beelzebubWrapperDeserialized = MAPPER.readValue(beelzebubWrapperJson, AnimalWrapper.class); + Animal beelzebubExtracted = beelzebubWrapperDeserialized.animal; + assertTrue(beelzebubExtracted instanceof Cat); + assertSame(beelzebubExtracted.getClass(), Cat.class); + assertEquals(beelzebub.name, beelzebubExtracted.name); + assertEquals(beelzebub.furColor, ((Cat) beelzebubExtracted).furColor); + assertEquals(beelzebub.getType(), beelzebubExtracted.getType()); + + @SuppressWarnings("unchecked") + List animalListDeserialized = MAPPER.readValue(animalListJson, List.class); + assertNotNull(animalListDeserialized); + assertTrue(animalListDeserialized.size() == 2); + Animal cat = MAPPER.convertValue(animalListDeserialized.get(0), Animal.class); + assertTrue(cat instanceof Cat); + assertSame(cat.getClass(), Cat.class); + Animal dog = MAPPER.convertValue(animalListDeserialized.get(1), Animal.class); + assertTrue(dog instanceof Dog); + assertSame(dog.getClass(), Dog.class); + } + + /** + * Cars - serialization tests for no abstract method or type variable in base class + */ + @Test + public void testExistingPropertySerializationCars() throws Exception + { + Map result = writeAndMap(MAPPER, camry); + assertEquals(3, result.size()); + assertEquals(camry.name, result.get("name")); + assertEquals(camry.exteriorColor, result.get("exteriorColor")); + assertEquals(camry.getType(), result.get("type")); + + result = writeAndMap(MAPPER, accord); + assertEquals(3, result.size()); + assertEquals(accord.name, result.get("name")); + assertEquals(accord.speakerCount, result.get("speakerCount")); + assertEquals(accord.getType(), result.get("type")); + + String camrySerialized = MAPPER.writeValueAsString(camry); + assertEquals(camrySerialized, camryJson); + + String accordSerialized = MAPPER.writeValueAsString(accord); + assertEquals(accordSerialized, accordJson); + + String carWrapperSerialized = MAPPER.writeValueAsString(camryWrapper); + assertEquals(carWrapperSerialized, camryWrapperJson); + + String carListSerialized = MAPPER.writeValueAsString(carList); + assertEquals(carListSerialized, carListJson); + } + + /** + * Cars - deserialization tests for no abstract method or type variable in base class + */ + @Test + public void testSimpleClassAsExistingPropertyDeserializationCars() throws Exception + { + Car camryDeserialized = MAPPER.readValue(camryJson, Camry.class); + assertTrue(camryDeserialized instanceof Camry); + assertSame(camryDeserialized.getClass(), Camry.class); + assertEquals(camry.name, camryDeserialized.name); + assertEquals(camry.exteriorColor, ((Camry) camryDeserialized).exteriorColor); + assertEquals(camry.getType(), ((Camry) camryDeserialized).getType()); + + CarWrapper camryWrapperDeserialized = MAPPER.readValue(camryWrapperJson, CarWrapper.class); + Car camryExtracted = camryWrapperDeserialized.car; + assertTrue(camryExtracted instanceof Camry); + assertSame(camryExtracted.getClass(), Camry.class); + assertEquals(camry.name, camryExtracted.name); + assertEquals(camry.exteriorColor, ((Camry) camryExtracted).exteriorColor); + assertEquals(camry.getType(), ((Camry) camryExtracted).getType()); + + @SuppressWarnings("unchecked") + List carListDeserialized = MAPPER.readValue(carListJson, List.class); + assertNotNull(carListDeserialized); + assertTrue(carListDeserialized.size() == 2); + Car result = MAPPER.convertValue(carListDeserialized.get(0), Car.class); + assertTrue(result instanceof Camry); + assertSame(result.getClass(), Camry.class); + + result = MAPPER.convertValue(carListDeserialized.get(1), Car.class); + assertTrue(result instanceof Accord); + assertSame(result.getClass(), Accord.class); + } + + // for [databind#1635]: simple usage + @Test + public void testExistingEnumTypeId() throws Exception + { + Bean1635 result = MAPPER.readValue(a2q("{'value':3, 'type':'A'}"), + Bean1635.class); + assertEquals(Bean1635A.class, result.getClass()); + Bean1635A bean = (Bean1635A) result; + assertEquals(3, bean.value); + assertEquals(ABC.A, bean.type); + } + + // for [databind#1635]: verify that `defaultImpl` does not block assignment of + // type id + @Test + public void testExistingEnumTypeIdViaDefault() throws Exception + { + Bean1635 result = MAPPER.readValue(a2q("{'type':'C'}"), + Bean1635.class); + assertEquals(Bean1635Default.class, result.getClass()); + assertEquals(ABC.C, result.type); + } + + // [databind#3271]: verify that `null` token does not become "null" String + @Test + public void testDeserializationWithValidType() throws Exception { + Shape3271 deserShape = MAPPER.readValue("{\"type\":\"square\"}", Shape3271.class); + assertEquals("square", deserShape.getType()); + } + + @Test + public void testDeserializationWithInvalidType() throws Exception { + Shape3271 deserShape = MAPPER.readValue("{\"type\":\"invalid\"}", Shape3271.class); + assertEquals("invalid", deserShape.getType()); + } + + @Test + public void testDeserializationNull() throws Exception { + Shape3271 deserShape = MAPPER.readValue("{\"type\":null}", Shape3271.class); + assertNull(deserShape.getType()); // error: "expected null, but was:" + } + + // [databind#3251]: Double vs BigDecimal + @Test + public void test3251WithNewProperty() throws Exception + { + GenericWrapperWithNew3251 wrapper = new GenericWrapperWithNew3251<>(123.5); + + String json = MAPPER.writeValueAsString(wrapper); + GenericWrapperWithNew3251 actualWrapper = MAPPER.readValue(json, GenericWrapperWithNew3251.class); + + assertThat(actualWrapper).satisfies(it -> assertThat(it.getValue()).isEqualTo(123.5)); + assertThat(actualWrapper.getValue()).isInstanceOf(Double.class); + assertThat(json).contains("\"value\":123.5"); + } + + @Test + public void test3251WithExistingProperty() throws Exception + { + GenericWrapperWithExisting3251 wrapper = new GenericWrapperWithExisting3251<>(123.5); + + String json = MAPPER.writeValueAsString(wrapper); + GenericWrapperWithExisting3251 actualWrapper = MAPPER.readValue(json, GenericWrapperWithExisting3251.class); + + assertThat(actualWrapper).satisfies(it -> assertThat(it.getValue()).isEqualTo(123.5)); + assertThat(actualWrapper.getValue()).isInstanceOf(Double.class); + assertThat(json).contains("\"value\":123.5"); + } + +} diff --git a/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithJsonTypeInfoSimpleClassName4061Test.java b/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithJsonTypeInfoSimpleClassName4061Test.java new file mode 100644 index 0000000000..c8c92094a5 --- /dev/null +++ b/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithJsonTypeInfoSimpleClassName4061Test.java @@ -0,0 +1,297 @@ +package tools.jackson.databind.jsontype; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonMerge; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.databind.DeserializationConfig; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.MapperFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.exc.InvalidTypeIdException; +import tools.jackson.databind.jsontype.impl.SimpleNameIdResolver; +import tools.jackson.databind.testutil.DatabindTestUtil; + +/** + * Test for [databind#4061] Add + * JsonTypeInfo.Id.SIMPLE_NAME + */ +public class SealedTypesWithJsonTypeInfoSimpleClassName4061Test extends DatabindTestUtil { + @JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME) + static sealed class InnerSuper4061 permits InnerSub4061A, InnerSub4061B { + } + + static final class InnerSub4061A extends InnerSuper4061 { + } + + static final class InnerSub4061B extends InnerSuper4061 { + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS) + static sealed class MinimalInnerSuper4061 permits MinimalInnerSub4061A, MinimalInnerSub4061B { + } + + static final class MinimalInnerSub4061A extends MinimalInnerSuper4061 { + } + + static final class MinimalInnerSub4061B extends MinimalInnerSuper4061 { + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME) + static sealed class MixedSuper4061 + permits MixedSub4061AForSealedClasses, MixedSub4061BForSealedClasses { + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS) + static sealed class MixedMinimalSuper4061 + permits MixedMinimalSub4061AForSealedClasses, MixedMinimalSub4061BForSealedClasses { + } + + static class Root { + @JsonMerge + public MergeChild child; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME) + static abstract sealed class MergeChild permits MergeChildA, MergeChildB { + } + + static final class MergeChildA extends MergeChild { + public String name; + } + + static final class MergeChildB extends MergeChild { + public String code; + } + + static class PolyWrapperForAlias { + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_ARRAY) + @JsonSubTypes({@JsonSubTypes.Type(value = AliasBean.class, name = "ab")}) + public Object value; + + protected PolyWrapperForAlias() {} + + public PolyWrapperForAlias(Object v) { + value = v; + } + } + + static class AliasBean { + @JsonAlias({"nm", "Name"}) + public String name; + int _xyz; + int _a; + + @JsonCreator + public AliasBean(@JsonProperty("a") @JsonAlias("A") int a) { + _a = a; + } + + @JsonAlias({"Xyz"}) + public void setXyz(int x) { + _xyz = x; + } + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME) + static sealed class DuplicateSuperClass permits DuplicateSubClassForSealedClasses, + tools.jackson.databind.jsontype.DuplicateSubClassForSealedClasses { + } + + static final class DuplicateSubClassForSealedClasses extends DuplicateSuperClass { + } + + /* + * /********************************************************** /* Unit tests + * /********************************************************** + */ + + private final ObjectMapper MAPPER = newJsonMapper(); + + // inner class that has contains dollar sign + @Test + public void testInnerClass() throws Exception { + String jsonStr = a2q("{'@type':'InnerSub4061A'}"); + + // ser + assertEquals(jsonStr, MAPPER.writeValueAsString(new InnerSub4061A())); + + // deser <- breaks! + InnerSuper4061 bean = MAPPER.readValue(jsonStr, InnerSuper4061.class); + assertInstanceOf(InnerSuper4061.class, bean); + } + + // inner class that has contains dollar sign + @Test + public void testMinimalInnerClass() throws Exception { + String jsonStr = + a2q("{'@c':'.SealedTypesWithJsonTypeInfoSimpleClassName4061Test$MinimalInnerSub4061A'}"); + + // ser + assertEquals(jsonStr, MAPPER.writeValueAsString(new MinimalInnerSub4061A())); + + // deser <- breaks! + MinimalInnerSuper4061 bean = MAPPER.readValue(jsonStr, MinimalInnerSuper4061.class); + assertInstanceOf(MinimalInnerSuper4061.class, bean); + assertNotNull(bean); + } + + // Basic : non-inner class, without dollar sign + @Test + public void testBasicClass() throws Exception { + String jsonStr = a2q("{'@type':'BasicSub4061A'}"); + + // ser + assertEquals(jsonStr, MAPPER.writeValueAsString(new BasicSub4061A())); + + // deser + BasicSuper4061 bean = MAPPER.readValue(jsonStr, BasicSuper4061.class); + assertInstanceOf(BasicSuper4061.class, bean); + assertInstanceOf(BasicSub4061A.class, bean); + + } + + // Mixed SimpleClassName : parent as inner, subtype as basic + @Test + public void testMixedClass() throws Exception { + String jsonStr = a2q("{'@type':'MixedSub4061AForSealedClasses'}"); + + // ser + assertEquals(jsonStr, MAPPER.writeValueAsString(new MixedSub4061AForSealedClasses())); + + // deser + MixedSuper4061 bean = MAPPER.readValue(jsonStr, MixedSuper4061.class); + assertInstanceOf(MixedSuper4061.class, bean); + assertInstanceOf(MixedSub4061AForSealedClasses.class, bean); + } + + // Mixed MinimalClass : parent as inner, subtype as basic + @Test + public void testMixedMinimalClass() throws Exception { + String jsonStr = a2q("{'@c':'.MixedMinimalSub4061AForSealedClasses'}"); + + // ser + assertEquals(jsonStr, MAPPER.writeValueAsString(new MixedMinimalSub4061AForSealedClasses())); + + // deser + MixedMinimalSuper4061 bean = MAPPER.readValue(jsonStr, MixedMinimalSuper4061.class); + assertInstanceOf(MixedMinimalSuper4061.class, bean); + assertInstanceOf(MixedMinimalSub4061AForSealedClasses.class, bean); + } + + @Test + public void testPolymorphicNewObject() throws Exception { + String jsonStr = "{\"child\": { \"@type\": \"MergeChildA\", \"name\": \"I'm child A\" }}"; + + Root root = MAPPER.readValue(jsonStr, Root.class); + + assertTrue(root.child instanceof MergeChildA); + assertEquals("I'm child A", ((MergeChildA) root.child).name); + } + + // case insenstive type name + @Test + public void testPolymorphicNewObjectCaseInsensitive() throws Exception { + String jsonStr = "{\"child\": { \"@type\": \"mergechilda\", \"name\": \"I'm child A\" }}"; + ObjectMapper mapper = + jsonMapperBuilder().enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES).build(); + + Root root = mapper.readValue(jsonStr, Root.class); + + assertTrue(root.child instanceof MergeChildA); + assertEquals("I'm child A", ((MergeChildA) root.child).name); + } + + @Test + public void testPolymorphicNewObjectUnknownTypeId() throws Exception { + try { + MAPPER.readValue("{\"child\": { \"@type\": \"UnknownChildA\", \"name\": \"I'm child A\" }}", + Root.class); + } catch (InvalidTypeIdException e) { + verifyException(e, "Could not resolve type id 'UnknownChildA' as a subtype of"); + } + } + + @Test + public void testAliasWithPolymorphic() throws Exception { + String jsonStr = a2q("{'value': ['ab', {'nm' : 'Bob', 'A' : 17} ] }"); + + PolyWrapperForAlias value = MAPPER.readValue(jsonStr, PolyWrapperForAlias.class); + + assertNotNull(value.value); + AliasBean bean = (AliasBean) value.value; + assertEquals("Bob", bean.name); + assertEquals(17, bean._a); + } + + @Test + public void testGetMechanism() { + final DeserializationConfig config = MAPPER.deserializationConfig(); + JavaType javaType = config.constructType(InnerSub4061B.class); + List namedTypes = new ArrayList<>(); + namedTypes.add(new NamedType(InnerSub4061A.class)); + namedTypes.add(new NamedType(InnerSub4061B.class)); + + SimpleNameIdResolver idResolver = + SimpleNameIdResolver.construct(config, javaType, namedTypes, false, true); + + assertEquals(JsonTypeInfo.Id.SIMPLE_NAME, idResolver.getMechanism()); + } + + @Test + public void testDuplicateNameLastOneWins() throws Exception { + String jsonStr = a2q("{'@type':'DuplicateSubClassForSealedClasses'}"); + + // deser + DuplicateSuperClass bean = MAPPER.readValue(jsonStr, DuplicateSuperClass.class); + assertInstanceOf(tools.jackson.databind.jsontype.DuplicateSubClassForSealedClasses.class, bean); + } +} + + +@JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME) +sealed class BasicSuper4061ForSealedTypes + permits BasicSub4061AForSealedTypes, BasicSub4061BForSealedTypes { +} + + +final class BasicSub4061AForSealedTypes extends BasicSuper4061ForSealedTypes { +} + + +final class BasicSub4061BForSealedTypes extends BasicSuper4061ForSealedTypes { +} + + +final class MixedSub4061AForSealedClasses + extends SealedTypesWithJsonTypeInfoSimpleClassName4061Test.MixedSuper4061 { +} + + +final class MixedSub4061BForSealedClasses + extends SealedTypesWithJsonTypeInfoSimpleClassName4061Test.MixedSuper4061 { +} + + +final class MixedMinimalSub4061AForSealedClasses + extends SealedTypesWithJsonTypeInfoSimpleClassName4061Test.MixedMinimalSuper4061 { +} + + +final class MixedMinimalSub4061BForSealedClasses + extends SealedTypesWithJsonTypeInfoSimpleClassName4061Test.MixedMinimalSuper4061 { +} + + +final class DuplicateSubClassForSealedClasses + extends SealedTypesWithJsonTypeInfoSimpleClassName4061Test.DuplicateSuperClass { +} diff --git a/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithPolymorphicDeductionTest.java b/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithPolymorphicDeductionTest.java new file mode 100644 index 0000000000..84c1a958e1 --- /dev/null +++ b/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithPolymorphicDeductionTest.java @@ -0,0 +1,336 @@ +package tools.jackson.databind.jsontype; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonValue; + +import tools.jackson.databind.*; +import tools.jackson.databind.exc.InvalidDefinitionException; +import tools.jackson.databind.exc.InvalidTypeIdException; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import static org.junit.jupiter.api.Assertions.*; + +import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.DEDUCTION; + +// for [databind#43], deduction-based polymorphism +public class SealedTypesWithPolymorphicDeductionTest extends DatabindTestUtil { + + @JsonTypeInfo(use = DEDUCTION) + // A general supertype with no properties - used for tests involving {} + sealed interface Feline permits Cat, Fleabag {} + + @JsonTypeInfo(use = DEDUCTION) + // A supertype containing common properties + public static sealed class Cat implements Feline permits DeadCat, LiveCat { + public String name; + } + + // Distinguished by its parent and a unique property + static final class DeadCat extends Cat { + public String causeOfDeath; + } + + // Distinguished by its parent and a unique property + static final class LiveCat extends Cat { + public boolean angry; + } + + // No distinguishing properties whatsoever + static final class Fleabag implements Feline { + // NO OP + } + + // Something to put felines in + static class Box { + public Feline feline; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) + static class Bean3711 { + @JsonValue + public String ser = "value"; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) + static enum Enum3711 { A, B } + + /* + /********************************************************** + /* Mock data + /********************************************************** + */ + + private static final String deadCatJson = a2q("{'causeOfDeath':'entropy','name':'Felix'}"); + private static final String liveCatJson = a2q("{'angry':true,'name':'Felix'}"); + private static final String luckyCatJson = a2q("{'name':'Felix','angry':true,'lives':8}"); + private static final String ambiguousCatJson = a2q("{'name':'Felix','age':2}"); + private static final String fleabagJson = a2q("{}"); + private static final String box1Json = a2q("{'feline':" + liveCatJson + "}"); + private static final String box2Json = a2q("{'feline':" + deadCatJson + "}"); + private static final String box3Json = a2q("{'feline':" + fleabagJson + "}"); + private static final String box4Json = a2q("{'feline':null}"); + private static final String box5Json = a2q("{}"); + private static final String arrayOfCatsJson = a2q("[" + liveCatJson + "," + deadCatJson + "]"); + private static final String mapOfCatsJson = a2q("{'live':" + liveCatJson + "}"); + + /* + /********************************************************** + /* Test methods + /********************************************************** + */ + + private final ObjectMapper MAPPER = jsonMapperBuilder() + .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) + .build(); + + @Test + public void testSimpleInference() throws Exception { + Cat cat = MAPPER.readValue(liveCatJson, Cat.class); + assertTrue(cat instanceof LiveCat); + assertSame(cat.getClass(), LiveCat.class); + assertEquals("Felix", cat.name); + assertTrue(((LiveCat)cat).angry); + + cat = MAPPER.readValue(deadCatJson, Cat.class); + assertTrue(cat instanceof DeadCat); + assertSame(cat.getClass(), DeadCat.class); + assertEquals("Felix", cat.name); + assertEquals("entropy", ((DeadCat)cat).causeOfDeath); + } + + @Test + public void testSimpleInferenceOfEmptySubtype() throws Exception { + // Given: + ObjectMapper mapper = MAPPER; + // When: + Feline feline = mapper.readValue(fleabagJson, Feline.class); + // Then: + assertTrue(feline instanceof Fleabag); + } + + @Test + public void testSimpleInferenceOfEmptySubtypeDoesntMatchNull() throws Exception { + // Given: + ObjectMapper mapper = MAPPER; + // When: + Feline feline = mapper.readValue("null", Feline.class); + // Then: + assertNull(feline); + } + + @Test + public void testCaseInsensitiveInference() throws Exception { + Cat cat = JsonMapper.builder() // Don't use shared mapper! + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) + .build() + .readValue(deadCatJson.toUpperCase(), Cat.class); + assertTrue(cat instanceof DeadCat); + assertSame(cat.getClass(), DeadCat.class); + assertEquals("FELIX", cat.name); + assertEquals("ENTROPY", ((DeadCat)cat).causeOfDeath); + } + + // TODO not currently supported +// public void testCaseInsensitivePerFieldInference() throws Exception { +// ObjectMapper mapper = JsonMapper.builder() // Don't use shared mapper! +// .configOverride(DeadCat.class) +// .setFormat(JsonFormat.Value.empty() +// .withFeature(JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)); +// Cat cat = mapper.readValue(deadCatJson.replace("causeOfDeath", "CAUSEOFDEATH"), Cat.class); +// assertTrue(cat instanceof DeadCat); +// assertSame(cat.getClass(), DeadCat.class); +// assertEquals("Felix", cat.name); +// assertEquals("Entropy", ((DeadCat)cat).causeOfDeath); +// } + + @Test + public void testContainedInference() throws Exception { + Box box = MAPPER.readValue(box1Json, Box.class); + assertTrue(box.feline instanceof LiveCat); + assertSame(box.feline.getClass(), LiveCat.class); + assertEquals("Felix", ((LiveCat)box.feline).name); + assertTrue(((LiveCat)box.feline).angry); + + box = MAPPER.readValue(box2Json, Box.class); + assertTrue(box.feline instanceof DeadCat); + assertSame(box.feline.getClass(), DeadCat.class); + assertEquals("Felix", ((DeadCat)box.feline).name); + assertEquals("entropy", ((DeadCat)box.feline).causeOfDeath); + } + + @Test + public void testContainedInferenceOfEmptySubtype() throws Exception { + Box box = MAPPER.readValue(box3Json, Box.class); + assertTrue(box.feline instanceof Fleabag); + + box = MAPPER.readValue(box4Json, Box.class); + assertNull(box.feline, "null != {}"); + + box = MAPPER.readValue(box5Json, Box.class); + assertNull(box.feline, " != {}"); + } + + @Test + public void testListInference() throws Exception { + JavaType listOfCats = defaultTypeFactory().constructParametricType(List.class, Cat.class); + List boxes = MAPPER.readValue(arrayOfCatsJson, listOfCats); + assertTrue(boxes.get(0) instanceof LiveCat); + assertTrue(boxes.get(1) instanceof DeadCat); + } + + @Test + public void testMapInference() throws Exception { + JavaType mapOfCats = defaultTypeFactory().constructParametricType(Map.class, String.class, Cat.class); + Map map = MAPPER.readValue(mapOfCatsJson, mapOfCats); + assertEquals(1, map.size()); + assertTrue(map.entrySet().iterator().next().getValue() instanceof LiveCat); + } + + @Test + public void testArrayInference() throws Exception { + Cat[] boxes = MAPPER.readValue(arrayOfCatsJson, Cat[].class); + assertTrue(boxes[0] instanceof LiveCat); + assertTrue(boxes[1] instanceof DeadCat); + } + + @Test + public void testIgnoreProperties() throws Exception { + Cat cat = MAPPER.readerFor(Cat.class) + .without(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .readValue(luckyCatJson); + assertTrue(cat instanceof LiveCat); + assertSame(cat.getClass(), LiveCat.class); + assertEquals("Felix", cat.name); + assertTrue(((LiveCat)cat).angry); + } + + @JsonTypeInfo(use = DEDUCTION) + // A general supertype with no properties - used for tests involving {} + sealed interface AmbiguousFeline permits AmbiguousCat, AmbiguousFleabag {} + + @JsonTypeInfo(use = DEDUCTION) + // A supertype containing common properties + public static sealed class AmbiguousCat implements AmbiguousFeline permits AmbiguousDeadCat, AmbiguousLiveCat, AmbiguousAnotherLiveCat { + public String name; + } + + // Distinguished by its parent and a unique property + static final class AmbiguousDeadCat extends AmbiguousCat { + public String causeOfDeath; + } + + // Distinguished by its parent and a unique property + static final class AmbiguousLiveCat extends AmbiguousCat { + public boolean angry; + } + + // No distinguishing properties whatsoever + static final class AmbiguousFleabag implements AmbiguousFeline { + // NO OP + } + + + static final class AmbiguousAnotherLiveCat extends AmbiguousCat { + public boolean angry; + } + + @Test + public void testAmbiguousClasses() throws Exception { + try { + ObjectMapper mapper = JsonMapper.builder() // Don't use shared mapper! + // .registerSubtypes(AmbiguousAnotherLiveCat.class) + .build(); + /*Cat cat =*/ mapper.readValue(liveCatJson, AmbiguousCat.class); + fail("Should not get here"); + } catch (InvalidDefinitionException e) { + verifyException(e, "Subtypes "); + verifyException(e, "have the same signature"); + verifyException(e, "cannot be uniquely deduced"); + } + } + + @Test + public void testAmbiguousProperties() throws Exception { + try { + /*Cat cat =*/ MAPPER.readValue(ambiguousCatJson, Cat.class); + fail("Should not get here"); + } catch (InvalidTypeIdException e) { + verifyException(e, "Cannot deduce unique subtype"); + } + } + + @Test + public void testFailOnInvalidSubtype() throws Exception { + // Given: + JsonMapper mapper = JsonMapper.builder() // Don't use shared mapper! + .disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE) + .build(); + // When: + Cat cat = mapper.readValue(ambiguousCatJson, Cat.class); + // Then: + assertNull(cat); + } + + @JsonTypeInfo(use = DEDUCTION, defaultImpl = Cat.class) + abstract static class CatMixin { + } + + @Test + public void testDefaultImpl() throws Exception { + // Given: + JsonMapper mapper = JsonMapper.builder() // Don't use shared mapper! + .addMixIn(Cat.class, CatMixin.class) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + // When: + Cat cat = mapper.readValue(ambiguousCatJson, Cat.class); + // Then: + // Even though "age":2 implies this was a failed subtype, we are instructed to fallback to Cat regardless. + assertTrue(cat instanceof Cat); + assertSame(Cat.class, cat.getClass()); + assertEquals("Felix", cat.name); + } + + @Test + public void testSimpleSerialization() throws Exception { + // Given: + JavaType listOfCats = defaultTypeFactory().constructParametricType(List.class, Cat.class); + List list = MAPPER.readValue(arrayOfCatsJson, listOfCats); + Cat cat = list.get(0); + // When: + String json = MAPPER.writeValueAsString(cat); + // Then: + assertEquals(liveCatJson, json); + } + + @Test + public void testListSerialization() throws Exception { + // Given: + JavaType listOfCats = defaultTypeFactory().constructParametricType(List.class, Cat.class); + List list = MAPPER.readValue(arrayOfCatsJson, listOfCats); + // When: + String json = MAPPER.writeValueAsString(list); + // Then: + assertEquals(arrayOfCatsJson, json); + } + + // [databind#3711] + @Test + public void testWithPojoAsJsonValue() throws Exception + { + assertEquals(q("value"), MAPPER.writeValueAsString(new Bean3711())); + } + + // [databind#3711] + @Test + public void testWithEnum() throws Exception + { + assertEquals(q("B"), MAPPER.writeValueAsString(Enum3711.B)); + } +} diff --git a/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithSubtypesTest.java b/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithSubtypesTest.java new file mode 100644 index 0000000000..86ad34162e --- /dev/null +++ b/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithSubtypesTest.java @@ -0,0 +1,402 @@ +package tools.jackson.databind.jsontype; + +import java.util.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; + +import tools.jackson.databind.*; +import tools.jackson.databind.exc.InvalidTypeIdException; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.testutil.DatabindTestUtil; +import tools.jackson.databind.testutil.NoCheckSubTypeValidator; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for detecting sealed types as subtypes. Originally copied from `TestSubtypes`. + */ +public class SealedTypesWithSubtypesTest extends DatabindTestUtil +{ + @JsonTypeInfo(use=JsonTypeInfo.Id.NAME) + static abstract sealed class SuperType permits SubB, SubC, SubD { + } + + @JsonTypeName("TypeB") + static final class SubB extends SuperType { + public int b = 1; + } + + static final class SubC extends SuperType { + public int c = 2; + } + + static final class SubD extends SuperType { + public int d; + } + + // "Empty" bean + @JsonTypeInfo(use=JsonTypeInfo.Id.NAME) + static abstract sealed class BaseBean permits EmptyBean { } + + static final class EmptyBean extends BaseBean { } + + static class EmptyNonFinal { } + + // Verify combinations + + static class PropertyBean + { + @JsonTypeInfo(use=JsonTypeInfo.Id.NAME) + public SuperType value; + + public PropertyBean() { this(null); } + public PropertyBean(SuperType v) { value = v; } + } + + @JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=As.PROPERTY, property="type") + static abstract sealed class BaseX permits ImplX, ImplY { } + + @JsonTypeName("x") + static final class ImplX extends BaseX { + public int x; + + public ImplX() { } + public ImplX(int x) { this.x = x; } + } + + @JsonTypeName("y") + static final class ImplY extends BaseX { + public int y; + } + + // DISABLED: Java does not allow "leaf" abstract sealed classes, since a class can't be both + // abstract and final. + // for [databind#919] testing + // @JsonTypeName("abs") + // abstract static class ImplAbs extends BaseX { + // } + + // [databind#663] + static class AtomicWrapper { + public BaseX value; + + public AtomicWrapper() { } + public AtomicWrapper(int x) { value = new ImplX(x); } + } + + // Verifying limits on sub-class ids + + static class DateWrapper { + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY) + public java.util.Date value; + } + + static class TheBomb { + public int a; + public TheBomb() { + throw new Error("Ka-boom!"); + } + } + + // [databind#1125] + + static class Issue1125Wrapper { + public Base1125 value; + + public Issue1125Wrapper() { } + public Issue1125Wrapper(Base1125 v) { value = v; } + } + + @JsonTypeInfo(use=JsonTypeInfo.Id.NAME, defaultImpl=Default1125.class) + static sealed class Base1125 permits Interm1125 { + public int a; + } + + static sealed class Interm1125 extends Base1125 permits Impl1125, Default1125{ + public int b; + } + + static final class Impl1125 extends Interm1125 { + public int c; + + public Impl1125() { } + public Impl1125(int a0, int b0, int c0) { + a = a0; + b = b0; + c = c0; + } + } + + static final class Default1125 extends Interm1125 { + public int def; + + Default1125() { } + public Default1125(int a0, int b0, int def0) { + a = a0; + b = b0; + def = def0; + } + } + + // [databind#1311] + @JsonTypeInfo(property = "type", use = JsonTypeInfo.Id.NAME, defaultImpl = Factory1311ImplA.class) + sealed interface Factory1311 permits Factory1311ImplA, Factory1311ImplB { } + + @JsonTypeName("implA") + static final class Factory1311ImplA implements Factory1311 { } + + @JsonTypeName("implB") + static final class Factory1311ImplB implements Factory1311 { } + + // [databind#2515] + @JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=As.PROPERTY, property="#type") + static abstract sealed class SuperTypeWithoutDefault permits Sub { } + + static final class Sub extends SuperTypeWithoutDefault { + public int a; + + public Sub(){} + public Sub(int a) { + this.a = a; + } + } + + static class POJOWrapper { + @JsonProperty + Sub sub1; + @JsonProperty + Sub sub2; + + public POJOWrapper(){} + public POJOWrapper(Sub sub1, Sub sub2) { + this.sub1 = sub1; + this.sub2 = sub2; + } + } + + /* + /********************************************************************** + /* Unit tests + /********************************************************************** + */ + + private final ObjectMapper MAPPER = newJsonMapper(); + + @Test + public void testPropertyWithSubtypes() throws Exception + { + // must register subtypes + ObjectMapper mapper = jsonMapperBuilder() + // .registerSubtypes(SubB.class, SubC.class, SubD.class) + .build(); + String json = mapper.writeValueAsString(new PropertyBean(new SubC())); + PropertyBean result = mapper.readValue(json, PropertyBean.class); + assertSame(SubC.class, result.value.getClass()); + } + + // also works via modules + @Test + public void testSubtypesViaModule() throws Exception + { + SimpleModule module = new SimpleModule(); + // module.registerSubtypes(SubB.class, SubC.class, SubD.class); + ObjectMapper mapper = jsonMapperBuilder() + .addModule(module) + .build(); + String json = mapper.writeValueAsString(new PropertyBean(new SubC())); + PropertyBean result = mapper.readValue(json, PropertyBean.class); + assertSame(SubC.class, result.value.getClass()); + + // and as per [databind#1653]: + module = new SimpleModule(); + List> l = new ArrayList<>(); + l.add(SubB.class); + l.add(SubC.class); + l.add(SubD.class); + // module.registerSubtypes(l); + mapper = jsonMapperBuilder() + .addModule(module) + .build(); + json = mapper.writeValueAsString(new PropertyBean(new SubC())); + result = mapper.readValue(json, PropertyBean.class); + assertSame(SubC.class, result.value.getClass()); + } + + @Test + public void testSerialization() throws Exception + { + // serialization can detect type name ok without anything extra: + SubB bean = new SubB(); + assertEquals("{\"@type\":\"TypeB\",\"b\":1}", MAPPER.writeValueAsString(bean)); + + // but we can override type name here too + ObjectMapper mapper = jsonMapperBuilder() + .registerSubtypes(new NamedType(SubB.class, "typeB")) + .build(); + assertEquals("{\"@type\":\"typeB\",\"b\":1}", mapper.writeValueAsString(bean)); + + // and default name ought to be simple class name; with context + assertEquals("{\"@type\":\"SealedTypesWithSubtypesTest$SubD\",\"d\":0}", mapper.writeValueAsString(new SubD())); + } + + @Test + public void testDeserializationNonNamed() throws Exception + { + ObjectMapper mapper = jsonMapperBuilder() + // .registerSubtypes(SubC.class) + .build(); + // default name should be unqualified class name + SuperType bean = mapper.readValue("{\"@type\":\"SealedTypesWithSubtypesTest$SubC\", \"c\":1}", SuperType.class); + assertSame(SubC.class, bean.getClass()); + assertEquals(1, ((SubC) bean).c); + } + + @Test + public void testDeserializatioNamed() throws Exception + { + ObjectMapper mapper = jsonMapperBuilder() + // .registerSubtypes(SubB.class) + .registerSubtypes(new NamedType(SubD.class, "TypeD")) + .build(); + + SuperType bean = mapper.readValue("{\"@type\":\"TypeB\", \"b\":13}", SuperType.class); + assertSame(SubB.class, bean.getClass()); + assertEquals(13, ((SubB) bean).b); + + // but we can also explicitly register name too + bean = mapper.readValue("{\"@type\":\"TypeD\", \"d\":-4}", SuperType.class); + assertSame(SubD.class, bean.getClass()); + assertEquals(-4, ((SubD) bean).d); + } + + @Test + public void testEmptyBean() throws Exception + { + // First, with annotations + ObjectMapper mapper = jsonMapperBuilder() + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, true) + .build(); + String json = mapper.writeValueAsString(new EmptyBean()); + assertEquals("{\"@type\":\"SealedTypesWithSubtypesTest$EmptyBean\"}", json); + + mapper = jsonMapperBuilder() + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .build(); + json = mapper.writeValueAsString(new EmptyBean()); + assertEquals("{\"@type\":\"SealedTypesWithSubtypesTest$EmptyBean\"}", json); + + // and then with defaults + mapper = jsonMapperBuilder() + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .activateDefaultTyping(NoCheckSubTypeValidator.instance, DefaultTyping.NON_FINAL) + .build(); + json = mapper.writeValueAsString(new EmptyNonFinal()); + assertEquals("[\"tools.jackson.databind.jsontype.SealedTypesWithSubtypesTest$EmptyNonFinal\",{}]", json); + } + + @Test + public void testErrorMessage() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + try { + mapper.readValue("{ \"type\": \"z\"}", BaseX.class); + fail("Should have failed"); + } catch (InvalidTypeIdException e) { + verifyException(e, "Could not resolve type id 'z' as a subtype of"); + verifyException(e, "known type ids = [x, y]"); + } + } + + @Test + public void testViaAtomic() throws Exception { + AtomicWrapper input = new AtomicWrapper(3); + String json = MAPPER.writeValueAsString(input); + + AtomicWrapper output = MAPPER.readValue(json, AtomicWrapper.class); + assertNotNull(output); + assertEquals(ImplX.class, output.value.getClass()); + assertEquals(3, ((ImplX) output.value).x); + } + + // Test to verify that base/impl restriction is applied to polymorphic handling + // even if class name is used as the id + @Test + public void testSubclassLimits() throws Exception + { + try { + MAPPER.readValue(a2q("{'value':['" + +TheBomb.class.getName()+"',{'a':13}] }"), DateWrapper.class); + fail("Should not pass"); + } catch (InvalidTypeIdException e) { + verifyException(e, "not a subtype"); + verifyException(e, TheBomb.class.getName()); + } catch (Exception e) { + fail("Should have hit `InvalidTypeIdException`, not `"+e.getClass().getName()+"`: "+e); + } + } + + // [databind#1125]: properties from base class too + + @Test + public void testIssue1125NonDefault() throws Exception + { + String json = MAPPER.writeValueAsString(new Issue1125Wrapper(new Impl1125(1, 2, 3))); + + Issue1125Wrapper result = MAPPER.readValue(json, Issue1125Wrapper.class); + assertNotNull(result.value); + assertEquals(Impl1125.class, result.value.getClass()); + Impl1125 impl = (Impl1125) result.value; + assertEquals(1, impl.a); + assertEquals(2, impl.b); + assertEquals(3, impl.c); + } + + @Test + public void testIssue1125WithDefault() throws Exception + { + Issue1125Wrapper result = MAPPER.readValue(a2q("{'value':{'a':3,'def':9,'b':5}}"), + Issue1125Wrapper.class); + assertNotNull(result.value); + assertEquals(Default1125.class, result.value.getClass()); + Default1125 impl = (Default1125) result.value; + assertEquals(3, impl.a); + assertEquals(5, impl.b); + assertEquals(9, impl.def); + } + + // [databind#2525] + public void testSerializationWithDuplicateRegisteredSubtypes() throws Exception { + ObjectMapper mapper = jsonMapperBuilder() + .registerSubtypes(new NamedType(Sub.class, "sub1")) + .registerSubtypes(new NamedType(Sub.class, "sub2")) + .build(); + + // the first registered type name is used for serialization + Sub sub = new Sub(15); + assertEquals("{\"#type\":\"sub1\",\"a\":15}", mapper.writeValueAsString(sub)); + } + + // [databind#2525] + public void testDeserializationWithDuplicateRegisteredSubtypes() throws Exception { + ObjectMapper mapper = jsonMapperBuilder() + // We can register the same class with different names + .registerSubtypes(new NamedType(Sub.class, "sub1")) + .registerSubtypes(new NamedType(Sub.class, "sub2")) + .build(); + + // fields of a POJO will be deserialized correctly according to their field name + POJOWrapper pojoWrapper = mapper.readValue("{\"sub1\":{\"#type\":\"sub1\",\"a\":10},\"sub2\":{\"#type\":\"sub2\",\"a\":50}}", POJOWrapper.class); + assertEquals(10, pojoWrapper.sub1.a); + assertEquals(50, pojoWrapper.sub2.a); + + // Instances of the same object can be deserialized with multiple names + SuperTypeWithoutDefault sub1 = mapper.readValue("{\"#type\":\"sub1\", \"a\":20}", SuperTypeWithoutDefault.class); + assertSame(Sub.class, sub1.getClass()); + assertEquals(20, ((Sub) sub1).a); + SuperTypeWithoutDefault sub2 = mapper.readValue("{\"#type\":\"sub2\", \"a\":30}", SuperTypeWithoutDefault.class); + assertSame(Sub.class, sub2.getClass()); + assertEquals(30, ((Sub) sub2).a); + } +} diff --git a/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithTypedDeserializationTest.java b/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithTypedDeserializationTest.java new file mode 100644 index 0000000000..f9a01f7878 --- /dev/null +++ b/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithTypedDeserializationTest.java @@ -0,0 +1,365 @@ +package tools.jackson.databind.jsontype; + +import java.util.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; + +import tools.jackson.core.JsonParser; + +import tools.jackson.databind.*; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import static org.junit.jupiter.api.Assertions.*; + +public class SealedTypesWithTypedDeserializationTest + extends DatabindTestUtil +{ + /* + /********************************************************** + /* Helper types + /********************************************************** + */ + + /** + * Polymorphic base class + */ + @JsonTypeInfo(use=Id.CLASS, include=As.PROPERTY, property="@classy") + static sealed abstract class Animal permits Dog, Cat, Fish, NullAnimal { + public String name; + + protected Animal(String n) { name = n; } + } + + @JsonTypeName("doggie") + static final class Dog extends Animal + { + public int boneCount; + + @JsonCreator + public Dog(@JsonProperty("name") String name) { + super(name); + } + + public void setBoneCount(int i) { boneCount = i; } + } + + @JsonTypeName("kitty") + static final class Cat extends Animal + { + public String furColor; + + @JsonCreator + public Cat(@JsonProperty("furColor") String c) { + super(null); + furColor = c; + } + + public void setName(String n) { name = n; } + } + + // Allow "empty" beans + @JsonTypeName("fishy") + static final class Fish extends Animal + { + @JsonCreator + public Fish() + { + super(null); + } + } + + // [databind#2467]: Allow missing "content" for as-array deserialization + @JsonDeserialize(using = NullAnimalDeserializer.class) + static final class NullAnimal extends Animal + { + public static final NullAnimal NULL_INSTANCE = new NullAnimal(); + + public NullAnimal() { + super(null); + } + } + + static class NullAnimalDeserializer extends ValueDeserializer { + @Override + public NullAnimal getNullValue(final DeserializationContext context) { + return NullAnimal.NULL_INSTANCE; + } + + @Override + public NullAnimal deserialize(final JsonParser parser, final DeserializationContext context) { + throw new UnsupportedOperationException(); + } + } + + static class AnimalContainer { + public Animal animal; + } + + // base class with no useful info + @JsonTypeInfo(use=Id.CLASS, include=As.WRAPPER_ARRAY) + static abstract sealed class DummyBase permits DummyImpl { + protected DummyBase(boolean foo) { } + } + + static final class DummyImpl extends DummyBase { + public int x; + + public DummyImpl() { super(true); } + } + + @JsonTypeInfo(use=Id.MINIMAL_CLASS, include=As.WRAPPER_OBJECT) + interface TypeWithWrapper { } + + @JsonTypeInfo(use=Id.CLASS, include=As.WRAPPER_ARRAY) + interface TypeWithArray { } + + static class Issue506DateBean { + @JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type2") + public Date date; + } + + static class Issue506NumberBean + { + @JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type3") + @JsonSubTypes({ @Type(Long.class), + @Type(Integer.class) }) + public Number number; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_ARRAY) + static sealed interface Issue1751ArrBase permits Issue1751ArrImpl { } + + @JsonTypeName("0") + static final class Issue1751ArrImpl implements Issue1751ArrBase { } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, + property = "type") + @JsonSubTypes({ @Type(value = Issue1751PropImpl.class, name = "1") }) + static interface Issue1751PropBase { } + + static class Issue1751PropImpl implements Issue1751PropBase { } + + /* + /********************************************************** + /* Unit tests + /********************************************************** + */ + + private final ObjectMapper MAPPER = newJsonMapper(); + + /** + * First things first, let's ensure we can serialize using + * class name, written as main-level property name + */ + @Test + public void testSimpleClassAsProperty() throws Exception + { + Animal a = MAPPER.readValue(asJSONObjectValueString(MAPPER, + "@classy", Cat.class.getName(), + "furColor", "tabby", "name", "Garfield"), Animal.class); + assertNotNull(a); + assertEquals(Cat.class, a.getClass()); + Cat c = (Cat) a; + assertEquals("Garfield", c.name); + assertEquals("tabby", c.furColor); + } + + // Test inclusion using wrapper style + @Test + public void testTypeAsWrapper() throws Exception + { + ObjectMapper m = jsonMapperBuilder() + .addMixIn(Animal.class, TypeWithWrapper.class) + .build(); + String JSON = "{\".SealedTypesWithTypedDeserializationTest$Dog\" : " + +asJSONObjectValueString(m, "name", "Scooby", "boneCount", "6")+" }"; + Animal a = m.readValue(JSON, Animal.class); + assertTrue(a instanceof Animal); + assertEquals(Dog.class, a.getClass()); + Dog d = (Dog) a; + assertEquals("Scooby", d.name); + assertEquals(6, d.boneCount); + } + + // Test inclusion using 2-element array + @Test + public void testTypeAsArray() throws Exception + { + ObjectMapper m = jsonMapperBuilder() + .addMixIn(Animal.class, TypeWithArray.class) + .build(); + // hmmh. Not good idea to rely on exact output, order may change. But... + String JSON = "[\""+Dog.class.getName()+"\", " + +asJSONObjectValueString(m, "name", "Martti", "boneCount", "11")+" ]"; + Animal a = m.readValue(JSON, Animal.class); + assertEquals(Dog.class, a.getClass()); + Dog d = (Dog) a; + assertEquals("Martti", d.name); + assertEquals(11, d.boneCount); + } + + // Use basic Animal as contents of a regular List + @Test + public void testListAsArray() throws Exception + { + // This time using PROPERTY style (default) again + String JSON = "[\n" + +asJSONObjectValueString(MAPPER, + "@classy", Cat.class.getName(), "name", "Hello", "furColor", "white") + +",\n" + // let's shuffle doggy's fields a bit for testing + +asJSONObjectValueString(MAPPER, + "boneCount", Integer.valueOf(1), + "@classy", Dog.class.getName(), + "name", "Bob" + ) + +",\n" + +asJSONObjectValueString(MAPPER, + "@classy", Fish.class.getName()) + +", null\n]"; + + JavaType expType = defaultTypeFactory().constructCollectionType(ArrayList.class, Animal.class); + List animals = MAPPER.readValue(JSON, expType); + assertNotNull(animals); + assertEquals(4, animals.size()); + Cat c = (Cat) animals.get(0); + assertEquals("Hello", c.name); + assertEquals("white", c.furColor); + Dog d = (Dog) animals.get(1); + assertEquals("Bob", d.name); + assertEquals(1, d.boneCount); + Fish f = (Fish) animals.get(2); + assertNotNull(f); + assertNull(animals.get(3)); + } + + @Test + public void testCagedAnimal() throws Exception + { + String jsonCat = asJSONObjectValueString(MAPPER, + "@classy", Cat.class.getName(), "name", "Nilson", "furColor", "black"); + String JSON = "{\"animal\":"+jsonCat+"}"; + + AnimalContainer cont = MAPPER.readValue(JSON, AnimalContainer.class); + assertNotNull(cont); + Animal a = cont.animal; + assertNotNull(a); + Cat c = (Cat) a; + assertEquals("Nilson", c.name); + assertEquals("black", c.furColor); + } + + /** + * Test that verifies that there are few limitations on polymorphic + * base class. + */ + @Test + public void testAbstractEmptyBaseClass() throws Exception + { + DummyBase result = MAPPER.readValue( + "[\""+DummyImpl.class.getName()+"\",{\"x\":3}]", DummyBase.class); + assertNotNull(result); + assertEquals(DummyImpl.class, result.getClass()); + assertEquals(3, ((DummyImpl) result).x); + } + + // [JACKSON-506], wrt Date + @Test + public void testIssue506WithDate() throws Exception + { + Issue506DateBean input = new Issue506DateBean(); + input.date = new Date(1234L); + + String json = MAPPER.writeValueAsString(input); + + Issue506DateBean output = MAPPER.readValue(json, Issue506DateBean.class); + assertEquals(input.date, output.date); + } + + // [JACKSON-506], wrt Number + @Test + public void testIssue506WithNumber() throws Exception + { + Issue506NumberBean input = new Issue506NumberBean(); + input.number = Long.valueOf(4567L); + + String json = MAPPER.writeValueAsString(input); + + Issue506NumberBean output = MAPPER.readValue(json, Issue506NumberBean.class); + assertEquals(input.number, output.number); + } + + private String asJSONObjectValueString(ObjectMapper mapper, Object... args) + { + LinkedHashMap map = new LinkedHashMap(); + for (int i = 0, len = args.length; i < len; i += 2) { + map.put(args[i], args[i+1]); + } + return mapper.writeValueAsString(map); + } + + // [databind#1751]: allow ints as ids too + @Test + public void testIntAsTypeId1751Array() throws Exception + { + Issue1751ArrBase value; + + // Should allow both String and Int: + value = MAPPER.readValue("[0, { }]", Issue1751ArrBase.class); + assertNotNull(value); + assertEquals(Issue1751ArrImpl.class, value.getClass()); + + value = MAPPER.readValue("[\"0\", { }]", Issue1751ArrBase.class); + assertNotNull(value); + assertEquals(Issue1751ArrImpl.class, value.getClass()); + } + + // [databind#1751]: allow ints as ids too + @Test + public void testIntAsTypeId1751Prop() throws Exception + { + Issue1751PropBase value; + + // Should allow both String and Int: + value = MAPPER.readValue("{\"type\" : \"1\"}", Issue1751PropBase.class); + assertNotNull(value); + assertEquals(Issue1751PropImpl.class, value.getClass()); + + value = MAPPER.readValue("{\"type\" : 1}", Issue1751PropBase.class); + assertNotNull(value); + assertEquals(Issue1751PropImpl.class, value.getClass()); + } + + // [databind#2467]: Allow missing "content" for as-array deserialization + @Test + public void testTypeAsArrayWithNullableType() throws Exception + { + ObjectMapper m = jsonMapperBuilder() + .addMixIn(Animal.class, TypeWithArray.class) + .build(); + Animal a = m.readValue( + "[\""+Fish.class.getName()+"\"]", Animal.class); + assertNull(a); + } + + // [databind#2467] + @Test + public void testTypeAsArrayWithCustomDeserializer() throws Exception + { + ObjectMapper m = jsonMapperBuilder() + .addMixIn(Animal.class, TypeWithArray.class) + .build(); + Animal a = m.readValue( + "[\""+NullAnimal.class.getName()+"\"]", Animal.class); + assertNotNull(a); + assertEquals(NullAnimal.class, a.getClass()); + NullAnimal c = (NullAnimal) a; + assertNull(c.name); + } +} diff --git a/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithTypedSerializationTest.java b/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithTypedSerializationTest.java new file mode 100644 index 0000000000..160a282a9a --- /dev/null +++ b/src/test/java/tools/jackson/databind/jsontype/SealedTypesWithTypedSerializationTest.java @@ -0,0 +1,149 @@ +package tools.jackson.databind.jsontype; + +import java.util.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.*; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.*; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import static org.junit.jupiter.api.Assertions.*; + +import static com.fasterxml.jackson.annotation.JsonTypeInfo.*; + +public class SealedTypesWithTypedSerializationTest + extends DatabindTestUtil +{ + /* + /********************************************************** + /* Helper types + /********************************************************** + */ + + /** + * Polymorphic base class + */ + @JsonTypeInfo(use=Id.CLASS, include=As.PROPERTY) + public static sealed abstract class Animal permits Dog, Cat { + public String name; + + protected Animal(String n) { name = n; } + } + + @JsonTypeName("doggie") + static final class Dog extends Animal + { + public int boneCount; + + private Dog() { super(null); } + public Dog(String name, int b) { + super(name); + boneCount = b; + } + } + + @JsonTypeName("kitty") + static final class Cat extends Animal + { + public String furColor; + + private Cat() { super(null); } + public Cat(String name, String c) { + super(name); + furColor = c; + } + } + + public class AnimalWrapper { + public Animal animal; + + public AnimalWrapper(Animal a) { animal = a; } + } + + @JsonTypeInfo(use=Id.MINIMAL_CLASS, include=As.WRAPPER_OBJECT) + interface TypeWithWrapper { } + + @JsonTypeInfo(use=Id.CLASS, include=As.WRAPPER_ARRAY) + interface TypeWithArray { } + + @JsonTypeInfo(use=Id.NAME) + @JsonTypeName("empty") + public class Empty { } + + @JsonTypeInfo(include=As.PROPERTY, use=Id.CLASS) + public class Super {} + public class A extends Super {} + public class B extends Super {} + + /* + /********************************************************** + /* Unit tests + /********************************************************** + */ + + private final ObjectMapper MAPPER = newJsonMapper(); + + /** + * First things first, let's ensure we can serialize using + * class name, written as main-level property name + */ + @Test + public void testSimpleClassAsProperty() throws Exception + { + Map result = writeAndMap(MAPPER, new Cat("Beelzebub", "tabby")); + assertEquals(3, result.size()); + assertEquals("Beelzebub", result.get("name")); + assertEquals("tabby", result.get("furColor")); + // should we try customized class name? + String classProp = Id.CLASS.getDefaultPropertyName(); + assertEquals(Cat.class.getName(), result.get(classProp)); + } + + /** + * Test inclusion using wrapper style + */ + @Test + public void testTypeAsWrapper() throws Exception + { + ObjectMapper m = jsonMapperBuilder() + .addMixIn(Animal.class, TypeWithWrapper.class) + .build(); + Map result = writeAndMap(m, new Cat("Venla", "black")); + // should get a wrapper; keyed by minimal class name ("Cat" here) + assertEquals(1, result.size()); + // minimal class name is prefixed by dot, and for inner classes it's bit longer + Map cat = (Map) result.get(".SealedTypesWithTypedSerializationTest$Cat"); + assertNotNull(cat); + assertEquals(2, cat.size()); + assertEquals("Venla", cat.get("name")); + assertEquals("black", cat.get("furColor")); + } + + /** + * Test inclusion using 2-element array + */ + @Test + public void testTypeAsArray() throws Exception + { + ObjectMapper m = jsonMapperBuilder() + .addMixIn(Animal.class, TypeWithArray.class) + .build(); + // hmmh. Not good idea to rely on exact output, order may change. But... + Map result = writeAndMap(m, new AnimalWrapper(new Dog("Amadeus", 7))); + // First level, wrapper + assertEquals(1, result.size()); + List l = (List) result.get("animal"); + assertNotNull(l); + assertEquals(2, l.size()); + assertEquals(Dog.class.getName(), l.get(0)); + Map doggie = (Map) l.get(1); + assertNotNull(doggie); + assertEquals(2, doggie.size()); + assertEquals("Amadeus", doggie.get("name")); + assertEquals(Integer.valueOf(7), doggie.get("boneCount")); + } +} + diff --git a/src/test/java/tools/jackson/databind/jsontype/TestSubtypes.java b/src/test/java/tools/jackson/databind/jsontype/TestSubtypes.java index c9e759edf2..03366e819a 100644 --- a/src/test/java/tools/jackson/databind/jsontype/TestSubtypes.java +++ b/src/test/java/tools/jackson/databind/jsontype/TestSubtypes.java @@ -382,22 +382,22 @@ public void testSerializationWithDuplicateRegisteredSubtypes() throws Exception // [databind#2525] public void testDeserializationWithDuplicateRegisteredSubtypes() throws Exception { ObjectMapper mapper = jsonMapperBuilder() - // We can register the same class with different names - .registerSubtypes(new NamedType(Sub.class, "sub1")) - .registerSubtypes(new NamedType(Sub.class, "sub2")) - .build(); - - // fields of a POJO will be deserialized correctly according to their field name - POJOWrapper pojoWrapper = mapper.readValue("{\"sub1\":{\"#type\":\"sub1\",\"a\":10},\"sub2\":{\"#type\":\"sub2\",\"a\":50}}", POJOWrapper.class); - assertEquals(10, pojoWrapper.sub1.a); - assertEquals(50, pojoWrapper.sub2.a); - - // Instances of the same object can be deserialized with multiple names - SuperTypeWithoutDefault sub1 = mapper.readValue("{\"#type\":\"sub1\", \"a\":20}", SuperTypeWithoutDefault.class); - assertSame(Sub.class, sub1.getClass()); - assertEquals(20, ((Sub) sub1).a); - SuperTypeWithoutDefault sub2 = mapper.readValue("{\"#type\":\"sub2\", \"a\":30}", SuperTypeWithoutDefault.class); - assertSame(Sub.class, sub2.getClass()); - assertEquals(30, ((Sub) sub2).a); - } + // We can register the same class with different names + .registerSubtypes(new NamedType(Sub.class, "sub1")) + .registerSubtypes(new NamedType(Sub.class, "sub2")) + .build(); + + // fields of a POJO will be deserialized correctly according to their field name + POJOWrapper pojoWrapper = mapper.readValue("{\"sub1\":{\"#type\":\"sub1\",\"a\":10},\"sub2\":{\"#type\":\"sub2\",\"a\":50}}", POJOWrapper.class); + assertEquals(10, pojoWrapper.sub1.a); + assertEquals(50, pojoWrapper.sub2.a); + + // Instances of the same object can be deserialized with multiple names + SuperTypeWithoutDefault sub1 = mapper.readValue("{\"#type\":\"sub1\", \"a\":20}", SuperTypeWithoutDefault.class); + assertSame(Sub.class, sub1.getClass()); + assertEquals(20, ((Sub) sub1).a); + SuperTypeWithoutDefault sub2 = mapper.readValue("{\"#type\":\"sub2\", \"a\":30}", SuperTypeWithoutDefault.class); + assertSame(Sub.class, sub2.getClass()); + assertEquals(30, ((Sub) sub2).a); + } }