Reputation: 7728
I have an Animal
class accepting a generic type parameter T
, as shown below:
public class Animal<T> {
private String type;
private T details;
// getters and setters
}
The type parameter can be Dog
or Cat
.
public class Dog {
private String name;
private boolean goodBoy;
// no-arg, all-args constructors, getters and setters
}
public class Cat {
private String name;
private boolean naughty;
// no-arg, all-args constructors, getters and setters
}
I am trying to deserialise the following JSON
.
{
"type": "dog",
"details": {
"name": "Marley",
"goodBoy": true
}
}
However, when I deserialise, the field details always gets deserialised as a LinkedHashMap
and not the particular implementation of the class.
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Animal<Dog> dog = new Animal<>();
dog.setType("dog");
dog.setDetails(new Dog("Marley", true));
String dogJson = mapper.writeValueAsString(dog);
Animal dogDeserialized = mapper.readValue(dogJson, Animal.class);
// dogDeserialized's details is LinkedHashMap
}
I cannot change the above classes, and so can't use annotations on the field. Is there a way I can specify the list of classes which the ObjectMapper
may deserialise the details
field to ?
Note that the value of the type
field is set to "dog" or "cat" for respective Dog
or Cat
classes.
Upvotes: 4
Views: 696
Reputation: 38625
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)
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:
Upvotes: 0
Reputation: 39978
Use TypeReference
to specify the type to Deserialized
Animal<Dog> dogDeserialized = mapper.readValue(
dogJson, new TypeReference<Animal<Dog>>() {});
Or instead of Dog
and Cat
you can have only one class with all the properties
public class AnimalDetails {
private String name;
private Boolean goodBoy;
private Boolean naughty;
}
And set ObjectMapper to ignore unknown properties in the JSON:
ObjectMapper mapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
So if it is Dog
class naughty
will be null
and if it is Cat
class goodBoy
will be null
Upvotes: 1