简体   繁体   中英

Jackson deserialize object or array

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, ie

"someObj" : []

Any help is appreciated!

Thanks!

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, ie implementing 1 custom deserializer per type, I'm all ears ;)

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

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM