Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jackson deserialize object or array

Tags:

I have a Jackson Question.

Is there a way to deserialize a property that may have two types, for some objects it appears like this

"someObj" : { "obj1" : 5, etc....} 

then for others it appears as an empty array, i.e.

"someObj" : [] 

Any help is appreciated!

Thanks!

like image 888
dardo Avatar asked Nov 29 '11 03:11

dardo


People also ask

How do you deserialize an array of objects in Java?

We can easily deserialize JSON Array into Java Array by using the readValue() method of the ObectMapper class. In the readValue() method, we pass two parameters, i.e., jsonString and Student[]. class.

How does Jackson deserialization work?

Jackson uses default (no argument) constructor to create object and then sets value using setters. so you only need @NoArgsConstructor and @Setter.

Does Jackson preserve array order?

Jackson mapper fill the ArrayList maintaining the order of JSON. If you want a different order you can use the annotation @JsonPropertyOrder.


2 Answers

Edit: Since Jackson 2.5.0, you can use DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_EMPTY_OBJECT to resolve your problem.

The solution Bruce provides has a few problems/disadvantages:

  • you'll need to duplicate that code for each type you need to deserialize that way
  • ObjectMapper should be reused since it caches serializers and deserializers and, thus, is expensive to create. See http://wiki.fasterxml.com/JacksonBestPracticesPerformance
  • if your array contains some values, you probably want let jackson to fail deserializing it because it means there was a problem when it got encoded and you should see and fix that asap.

Here is my "generic" solution for that problem:

public abstract class EmptyArrayAsNullDeserializer<T> extends JsonDeserializer<T> {    private final Class<T> clazz;    protected EmptyArrayAsNullDeserializer(Class<T> clazz) {     this.clazz = clazz;   }    @Override   public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {     ObjectCodec oc = jp.getCodec();     JsonNode node = oc.readTree(jp);     if (node.isArray() && !node.getElements().hasNext()) {       return null;     }     return oc.treeToValue(node, clazz);   } } 

then you still need to create a custom deserializer for each different type, but that's a lot easier to write and you don't duplicate any logic:

public class Thing2Deserializer extends EmptyArrayAsNullDeserializer<Thing2> {    public Thing2Deserializer() {     super(Thing2.class);   } } 

then you use it as usual:

@JsonDeserialize(using = Thing2Deserializer.class) 

If you find a way to get rid of that last step, i.e. implementing 1 custom deserializer per type, I'm all ears ;)

like image 117
fabien Avatar answered Sep 20 '22 13:09

fabien


Jackson doesn't currently have a built-in configuration to automatically handle this particular case, so custom deserialization processing is necessary.

Following is an example of what such custom deserialization might look like.

import java.io.IOException;  import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.JsonParser; import org.codehaus.jackson.JsonProcessingException; import org.codehaus.jackson.Version; import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; import org.codehaus.jackson.annotate.JsonMethod; import org.codehaus.jackson.map.DeserializationContext; import org.codehaus.jackson.map.JsonDeserializer; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.module.SimpleModule;  public class JacksonFoo {   public static void main(String[] args) throws Exception   {     // {"property1":{"property2":42}}     String json1 = "{\"property1\":{\"property2\":42}}";      // {"property1":[]}     String json2 = "{\"property1\":[]}";      SimpleModule module = new SimpleModule("", Version.unknownVersion());     module.addDeserializer(Thing2.class, new ArrayAsNullDeserializer());      ObjectMapper mapper = new ObjectMapper().setVisibility(JsonMethod.FIELD, Visibility.ANY).withModule(module);      Thing1 firstThing = mapper.readValue(json1, Thing1.class);     System.out.println(firstThing);     // output:     // Thing1: property1=Thing2: property2=42      Thing1 secondThing = mapper.readValue(json2, Thing1.class);     System.out.println(secondThing);     // output:      // Thing1: property1=null   } }  class Thing1 {   Thing2 property1;    @Override   public String toString()   {     return String.format("Thing1: property1=%s", property1);   } }  class Thing2 {   int property2;    @Override   public String toString()   {     return String.format("Thing2: property2=%d", property2);   } }  class ArrayAsNullDeserializer extends JsonDeserializer<Thing2> {   @Override   public Thing2 deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException   {     JsonNode node = jp.readValueAsTree();     if (node.isObject())       return new ObjectMapper().setVisibility(JsonMethod.FIELD, Visibility.ANY).readValue(node, Thing2.class);     return null;   } } 

(You could make use of DeserializationConfig.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY to force the input to always bind to a collection, but that's probably not the approach I'd take given how the problem is currently described.)

like image 23
Programmer Bruce Avatar answered Sep 22 '22 13:09

Programmer Bruce