Jackson deserialize object or array
Asked Answered
G

4

24

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!

Gabe answered 29/11, 2011 at 3:42 Comment(1)
I found this answer useful if you want to do this on a single propertyAlpaca
S
14

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.)

Scrivings answered 29/11, 2011 at 3:56 Comment(5)
Ok! Thanks, I kinda figured this was going to be the case...never can just be easy can it! Do you happen to know of a good tutorial on writing one?Gabe
no chance on fixing the original input, right? or, in the worst case, if that is the only case, what about a string replace from "[]" to "{}"Vesicate
I wish I could, it's a web service call I have no control over, from a battlefield bad company 2 website.Gabe
> Do you happen to know of a good tutorial on writing one? -- Answer updated with example.Scrivings
since jackson 2.5.0 you can use DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_EMPTY_OBJECT see @Crumhorn replyWeekender
C
23

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 ;)

Crumhorn answered 9/4, 2014 at 7:53 Comment(2)
This solution looks like what I need.. thanks.. however jackson is throwing a JsonMappingException: has no default (no arg) constructor when I try to use it. I've tried adding a default constructor to the base class but am not having any luck.Gigantic
Make sure the class you pass to jackson to deserialize has a public default constructor (not just its base class). In the example above, i deserialize with Thing2.class; Thing2's class has a public default constructor. Make sure all the sub-classes of the class you are trying to deserialize also have a public default constructor. In doubt, try with a simpler class with only primitive types and if it works you know it's a problem with one of your classes ;)Crumhorn
S
14

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.)

Scrivings answered 29/11, 2011 at 3:56 Comment(5)
Ok! Thanks, I kinda figured this was going to be the case...never can just be easy can it! Do you happen to know of a good tutorial on writing one?Gabe
no chance on fixing the original input, right? or, in the worst case, if that is the only case, what about a string replace from "[]" to "{}"Vesicate
I wish I could, it's a web service call I have no control over, from a battlefield bad company 2 website.Gabe
> Do you happen to know of a good tutorial on writing one? -- Answer updated with example.Scrivings
since jackson 2.5.0 you can use DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_EMPTY_OBJECT see @Crumhorn replyWeekender
Z
2

There’s another angle to tackle this problem more generically for objects that would be deserialized using the BeanDeserializer, by creating a BeanDeserializerModifier and registering it with your mapper. BeanDeserializerModifier is a sort of alternative to subclassing BeanDeserializerFactory, and it gives you a chance to return something other than the normal deserializer that would be used, or to modify it.

So, first create a new JsonDeserializer that can accept another deserializer when it’s being constructed, and then holds on to that serializer. In the deserialize method, you can check if you’re being passed a JsonParser that's currently pointing at a JsonToken.START_ARRAY. If you’re not passed JsonToken.START_ARRAY, then just use the default deserializer that was passed in to this custom deserialize when it was created.

Finally, make sure to implement ResolvableDeserializer, so that the default deserializer is properly attached to the context that your custom deserializer is using.

class ArrayAsNullDeserialzer extends JsonDeserializer implements ResolvableDeserializer {
    JsonDeserializer<?> mDefaultDeserializer;

    @Override
    /* Make sure the wrapped deserializer is usable in this deserializer's contexts */
    public void resolve(DeserializationContext ctxt) throws JsonMappingException  {
         ((ResolvableDeserializer) mDefaultDeserializer).resolve(ctxt);
    }

    /* Pass in the deserializer given to you by BeanDeserializerModifier */
    public ArrayAsNullDeserialzer(JsonDeserializer<?> defaultDeserializer) {
        mDefaultDeserializer = defaultDeserializer;
    }

    @Override
    public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        JsonToken firstToken = jp.getCurrentToken();
        if (firstToken == JsonToken.START_ARRAY) {
            //Optionally, fail if this is something besides an empty array
           return null;
        } else {
            return mDefaultDeserializer.deserialize(jp, ctxt);
        }
    }
}

Now that we have our generic deserializer hook, let’s create a modifier that can use it. This is easy, just implement the modifyDeserializer method in your BeanDeserializerModifier. You will be passed the deserializer that would have been used to deserialize the bean. It also passes you the BeanDesc that will be deserialized, so you can control here whether or not you want to handle [] as null for all types.

public class ArrayAsNullDeserialzerModifier extends BeanDeserializerModifier  {

    @Override
    public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        if ( true /* or check beanDesc to only do this for certain types, for example */ ) {
            return new ArrayAsNullDeserializer(deserializer);
        } else {
            return deserializer;
        }
    }
}

Finally, you’ll need to register your BeanDeserializerModifier with your ObjectMapper. To do this, create a module, and add the modifier in the setup (SimpleModules don’t seem to have a hook for this, unfortunately). You can read more about modules elsewhere, but here’s an example if you don’t already have a module to add to:

Module m = new Module() {
    @Override public String getModuleName() { return "MyMapperModule"; }
    @Override public Version version() { return Version.unknownVersion(); }
    @Override public void setupModule(Module.SetupContext context) {
        context.addBeanDeserializerModifier(new ArrayAsNullDeserialzerModifier());
    }
};
Zachery answered 31/10, 2014 at 8:30 Comment(0)
R
0

None of the other answers worked for me:

  • We can't use a modifier since the ObjectMapper cannot be modified and we use a @JsonDeserialize annotation to install the deserializer.
  • We don't have access to the ObjectMapper either.
  • We need the resulting Map to be properly typed, which didn't seem to work with ObjectCodec.treeToValue.

This is the solution that finally worked:

public class EmptyArrayToEmptyMapDeserializer extends JsonDeserializer<Map<String, SomeComplexType>> {
    @Override
    public Map<String, SomeComplexType> deserialize(JsonParser parser,
            DeserializationContext context) throws IOException {
        if (parser.getCurrentToken() == JsonToken.START_ARRAY) {
            // Not sure what the parser does with the following END_ARRAY token, probably ignores it somehow.
            return Map.of();
        }
        return context.readValue(parser, TypeFactory.defaultInstance().constructMapType(Map.class, String.class, SomeComplexType.class));
    }
}
Representative answered 21/4, 2020 at 15:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.