Reputation: 884
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
}
Map result:
{
id: 78,
rated: {
value: 10
}
}
Individually I can map both of these easily, but how do I do it generically?
Basically I want to map it to a class like:
public class Rating {
private int id;
private int rated;
...
public void setRated(?) {
// if value == false, set rated = -1;
// else decode "value" as rated
}
}
All of the polymorphic examples use @JsonTypeInfo
to map based on a property in the data, but I don't have that option in this case.
@JsonProperty("rated")
public void setRating(JsonNode ratedNode) {
JsonNode valueNode = ratedNode.get("value");
// if the node doesn't exist then it's the boolean value
if (valueNode == null) {
// Use a default value
this.rating = -1;
} else {
// Convert the value to an integer
this.rating = valueNode.asInt();
}
}
Upvotes: 17
Views: 27038
Reputation: 116620
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 instanceof 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.
Upvotes: 31
Reputation: 55
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; }
Upvotes: 3
Reputation: 38710
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
}
Upvotes: 4
Reputation: 65869
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.
Upvotes: 1