I have a result from a web service that returns either a boolean value or a singleton map, e.g.
Boolean result:
{
id: 24428,
rated: false
}
<
I asked a similar question - JSON POJO consumer of polymorphic objects
You have to write your own deserialiser
that gets a look-in during the deserialise process and decides what to do depending on the data.
There may be other easier methods but this method worked well for me.
You have to write your own deserializer. It could look like this:
@SuppressWarnings("unchecked")
class RatingJsonDeserializer extends JsonDeserializer<Rating> {
@Override
public Rating deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Map<String, Object> map = jp.readValueAs(Map.class);
Rating rating = new Rating();
rating.setId(getInt(map, "id"));
rating.setRated(getRated(map));
return rating;
}
private int getInt(Map<String, Object> map, String propertyName) {
Object object = map.get(propertyName);
if (object instanceof Number) {
return ((Number) object).intValue();
}
return 0;
}
private int getRated(Map<String, Object> map) {
Object object = map.get("rated");
if (object instanceof Boolean) {
if (((Boolean) object).booleanValue()) {
return 0; // or throw exception
}
return -1;
}
if (object instanceof Map) {
return getInt(((Map<String, Object>) object), "value");
}
return 0;
}
}
Now you have to tell Jackson to use this deserializer for Rating
class:
@JsonDeserialize(using = RatingJsonDeserializer.class)
class Rating {
...
}
Simple usage:
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.readValue(json, Rating.class));
Above program prints:
Rating [id=78, rated=10]
for JSON:
{
"id": 78,
"rated": {
"value": 10
}
}
and prints:
Rating [id=78, rated=-1]
for JSON:
{
"id": 78,
"rated": false
}
I found a nice article on the subject: http://programmerbruce.blogspot.com/2011/05/deserialize-json-with-jackson-into.html
I think that the approach of parsing into object, is possibly problematic, because when you send it, you send a string. I am not sure it is an actual issue, but it sounds like some possible unexpected behavior. example 5 and 6 show that you can use inheritance for this.
Example:
Example 6: Simple Deserialization Without Type Element To Container Object With Polymorphic Collection
Some real-world JSON APIs have polymorphic type members, but don't include type elements (unlike the JSON in the previous examples). Deserializing such sources into polymorphic collections is a bit more involved. Following is one relatively simple solution. (This example includes subsequent serialization of the deserialized Java structure back to input JSON, but the serialization is relatively uninteresting.)
// input and output: // { // "animals": // [ // {"name":"Spike","breed":"mutt","leash_color":"red"}, // {"name":"Fluffy","favorite_toy":"spider ring"}, // {"name":"Baldy","wing_span":"6 feet", // "preferred_food":"wild salmon"} // ] // } import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.JsonParser; import org.codehaus.jackson.JsonProcessingException; import org.codehaus.jackson.Version; import org.codehaus.jackson.map.DeserializationContext; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.deser.StdDeserializer; import org.codehaus.jackson.map.module.SimpleModule; import org.codehaus.jackson.node.ObjectNode; import fubar.CamelCaseNamingStrategy; public class Foo { public static void main(String[] args) throws Exception { AnimalDeserializer deserializer = new AnimalDeserializer(); deserializer.registerAnimal("leash_color", Dog.class); deserializer.registerAnimal("favorite_toy", Cat.class); deserializer.registerAnimal("wing_span", Bird.class); SimpleModule module = new SimpleModule("PolymorphicAnimalDeserializerModule", new Version(1, 0, 0, null)); module.addDeserializer(Animal.class, deserializer); ObjectMapper mapper = new ObjectMapper(); mapper.setPropertyNamingStrategy( new CamelCaseNamingStrategy()); mapper.registerModule(module); Zoo zoo = mapper.readValue(new File("input_6.json"), Zoo.class); System.out.println(mapper.writeValueAsString(zoo)); } } class AnimalDeserializer extends StdDeserializer<Animal> { private Map<String, Class<? extends Animal>> registry = new HashMap<String, Class<? extends Animal>>(); AnimalDeserializer() { super(Animal.class); } void registerAnimal(String uniqueAttribute, Class<? extends Animal> animalClass) { registry.put(uniqueAttribute, animalClass); } @Override public Animal deserialize( JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { ObjectMapper mapper = (ObjectMapper) jp.getCodec(); ObjectNode root = (ObjectNode) mapper.readTree(jp); Class<? extends Animal> animalClass = null; Iterator<Entry<String, JsonNode>> elementsIterator = root.getFields(); while (elementsIterator.hasNext()) { Entry<String, JsonNode> element=elementsIterator.next(); String name = element.getKey(); if (registry.containsKey(name)) { animalClass = registry.get(name); break; } } if (animalClass == null) return null; return mapper.readValue(root, animalClass); } } class Zoo { public Collection<Animal> animals; } abstract class Animal { public String name; } class Dog extends Animal { public String breed; public String leashColor; } class Cat extends Animal { public String favoriteToy; } class Bird extends Animal { public String wingSpan; public String preferredFood; }
No no no. You do NOT have to write a custom deserializer. Just use "untyped" mapping first:
public class Response {
public long id;
public Object rated;
}
// OR
public class Response {
public long id;
public JsonNode rated;
}
Response r = mapper.readValue(source, Response.class);
which gives value of Boolean
or java.util.Map
for "rated" (with first approach); or a JsonNode
in second case.
From that, you can either access data as is, or, perhaps more interestingly, convert to actual value:
if (r.rated instanced Boolean) {
// handle that
} else {
ActualRated actual = mapper.convertValue(r.rated, ActualRated.class);
}
// or, if you used JsonNode, use "mapper.treeToValue(ActualRated.class)
There are other kinds of approaches too -- using creator "ActualRated(boolean)", to let instance constructed either from POJO, or from scalar. But I think above should work.