TestTypedDeserialization.java
package com.fasterxml.jackson.databind.jsontype;
import java.io.IOException;
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 com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.*;
public class TestTypedDeserialization
extends DatabindTestUtil
{
/*
/**********************************************************
/* Helper types
/**********************************************************
*/
/**
* Polymorphic base class
*/
@JsonTypeInfo(use=Id.CLASS, include=As.PROPERTY, property="@classy")
static abstract class Animal {
public String name;
protected Animal(String n) { name = n; }
}
@JsonTypeName("doggie")
static 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 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 class Fish extends Animal
{
@JsonCreator
public Fish()
{
super(null);
}
}
// [databind#2467]: Allow missing "content" for as-array deserialization
@JsonDeserialize(using = NullAnimalDeserializer.class)
static class NullAnimal extends Animal
{
public static final NullAnimal NULL_INSTANCE = new NullAnimal();
public NullAnimal() {
super(null);
}
}
static class NullAnimalDeserializer extends JsonDeserializer<NullAnimal> {
@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 class DummyBase {
protected DummyBase(boolean foo) { }
}
static 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)
@JsonSubTypes({ @Type(value = Issue1751ArrImpl.class, name = "0") })
static interface Issue1751ArrBase { }
static 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("@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 = new ObjectMapper();
m.addMixIn(Animal.class, TypeWithWrapper.class);
String JSON = "{\".TestTypedDeserialization$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 = new ObjectMapper();
m.addMixIn(Animal.class, TypeWithArray.class);
// 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
{
ObjectMapper m = MAPPER;
// This time using PROPERTY style (default) again
String JSON = "[\n"
+asJSONObjectValueString(m, "@classy", Cat.class.getName(), "name", "Hello", "furColor", "white")
+",\n"
// let's shuffle doggy's fields a bit for testing
+asJSONObjectValueString(m,
"boneCount", Integer.valueOf(1),
"@classy", Dog.class.getName(),
"name", "Bob"
)
+",\n"
+asJSONObjectValueString(m, "@classy", Fish.class.getName())
+", null\n]";
JavaType expType = defaultTypeFactory().constructCollectionType(ArrayList.class, Animal.class);
List<Animal> animals = m.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);
}
// [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 = new ObjectMapper();
m.addMixIn(Animal.class, TypeWithArray.class);
Animal a = m.readValue(
"[\""+Fish.class.getName()+"\"]", Animal.class);
assertNull(a);
}
// [databind#2467]
@Test
public void testTypeAsArrayWithCustomDeserializer() throws Exception
{
ObjectMapper m = new ObjectMapper();
m.addMixIn(Animal.class, TypeWithArray.class);
Animal a = m.readValue(
"[\""+NullAnimal.class.getName()+"\"]", Animal.class);
assertNotNull(a);
assertEquals(NullAnimal.class, a.getClass());
NullAnimal c = (NullAnimal) a;
assertNull(c.name);
}
private String asJSONObjectValueString(Object... args)
throws IOException
{
return asJSONObjectValueString(sharedMapper(), args);
}
private String asJSONObjectValueString(ObjectMapper m, Object... args)
throws IOException
{
LinkedHashMap<Object,Object> map = new LinkedHashMap<Object,Object>();
for (int i = 0, len = args.length; i < len; i += 2) {
map.put(args[i], args[i+1]);
}
return m.writeValueAsString(map);
}
}