Because you can not change POJO
model you need to implement custom deserialiser and handle types manually. Custom deserialiser could look like below:
class AnimalJsonDeserializer extends JsonDeserializer<Animal> {
private Map<String, Class> availableTypes = new HashMap<>();
public AnimalJsonDeserializer() {
availableTypes.put("cat", Cat.class);
availableTypes.put("dog", Dog.class);
}
@Override
public Animal deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException {
ObjectNode root = parser.readValueAsTree();
JsonNode type = getProperty(parser, root, "type");
Animal<Object> animal = new Animal<>();
animal.setType(type.asText());
Class<?> pojoClass = availableTypes.get(animal.getType());
if (pojoClass == null) {
throw new JsonMappingException(parser, "Class is not found for " + animal.getType());
}
JsonNode details = getProperty(parser, root, "details");
animal.setDetails(parser.getCodec().treeToValue(details, pojoClass));
return animal;
}
private JsonNode getProperty(JsonParser parser, ObjectNode root, String property) throws JsonMappingException {
JsonNode value = root.get(property);
if (value.isMissingNode() || value.isNull()) {
throw new JsonMappingException(parser, "No " + property + " field!");
}
return value;
}
}
We need to use SimpleModule
class and register deserialiser for Animal
class. Example usage:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class JsonApp {
public static void main(String[] args) throws Exception {
SimpleModule animalModule = new SimpleModule();
animalModule.addDeserializer(Animal.class, new AnimalJsonDeserializer());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(animalModule);
Animal<Dog> dog = new Animal<>();
dog.setType("dog");
dog.setDetails(new Dog("Marley", true));
Animal<Cat> cat = new Animal<>();
cat.setType("cat");
cat.setDetails(new Cat("Tom", false));
Animal<Dog> husky = new Animal<>();
husky.setType("husky");
husky.setDetails(new Dog("Sib", true));
for (Animal animal : new Animal[]{dog, cat, husky}) {
String json = mapper.writeValueAsString(animal);
System.out.println("JSON: " + json);
System.out.println("Deserialized: " + mapper.readValue(json, Animal.class));
System.out.println();
}
}
}
Above code prints:
JSON: {"type":"dog","details":{"name":"Marley","goodBoy":true}}
Deserialized: Animal{type='dog', details=Dog{name='Marley', goodBoy=true}}
JSON: {"type":"cat","details":{"name":"Tom","naughty":false}}
Deserialized: Animal{type='cat', details=Cat{name='Tom', naughty=false}}
JSON: {"type":"husky","details":{"name":"Sib","goodBoy":true}}
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Class is not found for husky
at [Source: (String)"{"type":"husky","details":{"name":"Sib","goodBoy":true}}"; line: 1, column: 56]
at AnimalJsonDeserializer.deserialize(JsonApp.java:69)
at AnimalJsonDeserializer.deserialize(JsonApp.java:50)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3004)
at com.celoxity.JsonApp.main(JsonApp.java:44)
MixIn feature could be possible if you could change classes.
If you can not change source classes you can always use Mix-in
feature which allows to create new interface with similar method and annotate all required classes appropriately. In your case we need to remove also type
property which will be handled automatically by Jackson
. Your example, after changes, could look like below:
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonApp {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Animal.class, AnimalMixIn.class);
Animal<Dog> dog = new Animal<>();
dog.setDetails(new Dog("Marley", true));
String dogJson = mapper.writeValueAsString(dog);
System.out.println(dogJson);
Animal dogDeserialized = mapper.readValue(dogJson, Animal.class);
System.out.println(dogDeserialized);
}
}
interface AnimalMixIn {
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", include = JsonTypeInfo.As.EXTERNAL_PROPERTY)
@JsonSubTypes(value = {
@JsonSubTypes.Type(value = Dog.class, name = "dog"),
@JsonSubTypes.Type(value = Cat.class, name = "cat")})
Object getDetails();
}
class Animal<T> {
private T details;
public T getDetails() {
return details;
}
public void setDetails(T details) {
this.details = details;
}
@Override
public String toString() {
return "Animal{details=" + details + '}';
}
}
Cat
and Dogs
classes stay the same. Above code prints:
{"details":{"name":"Marley","goodBoy":true},"type":"dog"}
Animal{details=Dog{name='Marley', goodBoy=true}}
See also: