Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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();
    }
}
like image 657
Omertron Avatar asked Nov 22 '13 10:11

Omertron


People also ask

Can Jackson ObjectMapper be reused?

ObjectMapper is the most important class in Jackson API that provides readValue() and writeValue() methods to transform JSON to Java Object and Java Object to JSON. ObjectMapper class can be reused and we can initialize it once as Singleton object.

What is Jackson object serialization?

Jackson is a solid and mature JSON serialization/deserialization library for Java. The ObjectMapper API provides a straightforward way to parse and generate JSON response objects with a lot of flexibility.

Which methods does Jackson rely upon to deserialize a JSON formatted string *?

The @JsonSetter annotation tells Jackson to deserialize the JSON into Java object using the name given in the setter method. Use this annotation when your JSON property names are different to the fields of the Java object class, and you want to map them.

Is ObjectMapper thread safe?

Mapper instances are fully thread-safe provided that ALL configuration of the instance occurs before ANY read or write calls.


2 Answers

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.

like image 193
StaxMan Avatar answered Oct 09 '22 16:10

StaxMan


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
}
like image 40
Michał Ziober Avatar answered Oct 09 '22 16:10

Michał Ziober