Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deserializing fails for a class implementing Collection with Jackson

Tags:

java

json

jackson

I have the following JSON:

{
  "item": [
    { "foo": 1 },
    { "foo": 2 }
  ]
} 

This is basically an object that contains a collection of items.

So I made a class to deserialize that:

public class ItemList {
  @JsonProperty("item")
  List<Item> items;

  // Getters, setters & co.
  // ...
}

Everything is working nicely up to this point.

Now, To make my life easier somewhere else, I decided that it would be nice to be able to iterate on the ItemList object and let it implement the Collection interface.

So basically my class became:

public class ItemList implements Collection<Item>, Iterable<Item> {
  @JsonProperty("item")
  List<Item> items;

  // Getters, setters & co.

  // Generated all method delegates to items. For instance:
  public Item get(int position) {
    return items.get(position);
  }
}

The implementation works properly and nicely. However, the deserialization now fails.

Looks like Jackson is getting confused:

com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of com.example.ItemList out of START_OBJECT token

I have tried to add @JsonDeserialize(as=ItemList.class) but it did not do the trick.

What's the way to go?

like image 845
Vincent Mimoun-Prat Avatar asked Jul 22 '14 17:07

Vincent Mimoun-Prat


2 Answers

Obviously it does not work because Jackson uses the standard collection deserialiser for Java collection types which knows nothing about ItemList properties.

It is possible to make it work but not in a very elegant way. You need to configure ObjectMapper to replace the default collection deserialiser on a bean deserialiser created manually for the corresponding type. I have written an example that does this in BeanDeserializerModifier for all the classes annotated with a custom annotation.

Note that I have to override ObjectMapper to get access to a protected method createDeserializationContext of ObjectMapper to create a proper deserialisation context since the bean modifier does not have access to it.

Here is the code:

public class JacksonCustomList {
    public static final String JSON = "{\n" +
            "  \"item\": [\n" +
            "    { \"foo\": 1 },\n" +
            "    { \"foo\": 2 }\n" +
            "  ]\n" +
            "} ";

    @Retention(RetentionPolicy.RUNTIME)
    public static @interface PreferBeanDeserializer {

    }

    public static class Item {
        public int foo;

        @Override
        public String toString() {
            return String.valueOf(foo);
        }
    }

    @PreferBeanDeserializer
    public static class ItemList extends ArrayList<Item> {
        @JsonProperty("item")
        public List<Item> items;

        @Override
        public String toString() {
            return items.toString();
        }
    }

    public static class Modifier extends BeanDeserializerModifier {
        private final MyObjectMapper mapper;

        public Modifier(final MyObjectMapper mapper) {
            this.mapper = mapper;
        }

        @Override
        public JsonDeserializer<?> modifyCollectionDeserializer(
                final DeserializationConfig config,
                final CollectionType type,
                final BeanDescription beanDesc,
                final JsonDeserializer<?> deserializer) {
            if (type.getRawClass().getAnnotation(PreferBeanDeserializer.class) != null) {
                DeserializationContext context = mapper.createContext(config);
                try {
                    return context.getFactory().createBeanDeserializer(context, type, beanDesc);
                } catch (JsonMappingException e) {
                   throw new IllegalStateException(e);
                }

            }
            return super.modifyCollectionDeserializer(config, type, beanDesc, deserializer);
        }
    }

    public static class MyObjectMapper extends ObjectMapper {
        public DeserializationContext createContext(final DeserializationConfig cfg) {
            return super.createDeserializationContext(getDeserializationContext().getParser(), cfg);
        }
    }

    public static void main(String[] args) throws IOException {
        final MyObjectMapper mapper = new MyObjectMapper();
        SimpleModule module = new SimpleModule();
        module.setDeserializerModifier(new Modifier(mapper));

        mapper.registerModule(module);
        System.out.println(mapper.readValue(JSON, ItemList.class));
    }

}
like image 156
Alexey Gavrilov Avatar answered Oct 23 '22 23:10

Alexey Gavrilov


If you consider the item property to be the root value, you can than change your ItemList class as follows, using the @JsonRootName annotation:

@JsonRootName("item")
public class ItemList implements Collection<Item>, Iterable<Item> {
    private List<Item> items = new ArrayList<>();

    public Item get(int position) {
        return items.get(position);
    }

    // implemented methods deferring to delegate
    // ...
}

If you then activate the UNWRAP_ROOT_VALUE deserialization feature, things work as expected:

String json = "{\"item\": [{\"foo\": 1}, {\"foo\": 2}]}";
ObjectMapper mapper = new ObjectMapper();
ObjectReader reader = mapper.reader(ItemList.class);

ItemList itemList = reader
        .with(DeserializationFeature.UNWRAP_ROOT_VALUE)
        .readValue(json);

Serialization works equally well, with the WRAP_ROOT_VALUE serialization feature enabled:

ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer();

Item item1 = new Item();
item1.setFoo(1);

Item item2 = new Item();
item2.setFoo(2);

ItemList itemList = new ItemList();
itemList.add(item1);
itemList.add(item2);

String json = writer
        .with(SerializationFeature.WRAP_ROOT_VALUE)
        .writeValueAsString(itemList);

// json contains {"item":[{"foo":1},{"foo":2}]}

This solution will obviously not suffice if your ItemList contains additional properties (other than the actual list) that will also need to be serialized/deserialized.

like image 42
Robby Cornelissen Avatar answered Oct 23 '22 23:10

Robby Cornelissen