11

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?

Vincent Mimoun-Prat
  • 28,208
  • 16
  • 81
  • 124

2 Answers2

5

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

}
Alexey Gavrilov
  • 10,593
  • 2
  • 38
  • 48
  • 1
    +1 Nice. I've spent a couple of hours trying to come up with a similar solution using `BeanDeserializerModifier.modifyCollectionDeserializer`, but couldn't figure out how to obtain a `DeserializationContext`. – Robby Cornelissen Jul 27 '14 at 03:45
  • Looks like an elegant way to handle that, I'll give it a try. – Vincent Mimoun-Prat Jul 27 '14 at 09:59
  • 1
    One suggestion: instead of sub-classing `ObjectMapper`, you should be able to combine `ContextualDeserializer` with custom Collection deserializer: this gives you access to find/modify value deserializer that is needed, and also guarantees it will be properly initialized. It may make sense to have a look at default collection serializers to handle some special cases (support for polymorphic types). – StaxMan Jul 30 '14 at 17:49
2

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.

Robby Cornelissen
  • 91,784
  • 22
  • 134
  • 156