Omertron
Omertron

Reputation: 884

Jackson deserialization of type with different objects

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.


EDIT
The updated section of code:

@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

Answers (4)

StaxMan
StaxMan

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

Asaf Bartov
Asaf Bartov

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

Michał Ziober
Michał Ziober

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

OldCurmudgeon
OldCurmudgeon

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

Related Questions