Дезертирование не выполняется для класса, реализующего Collection with Jackson

У меня есть следующий JSON:

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

Это в основном объект, содержащий набор элементов.

Итак, я создал класс для десериализации:

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

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

Все работает хорошо до этого момента.

Теперь, чтобы облегчить мою жизнь в другом месте, я решил, что было бы неплохо иметь возможность итерации объекта ItemList и позволить ему реализовать интерфейс Collection.

Итак, в основном мой класс стал:

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

Реализация работает правильно и красиво. Однако десериализация теперь терпит неудачу.

Похоже, Джексон путается:

com.fasterxml.jackson.databind.JsonMappingException: не удается десериализовать экземпляр com.example.ItemList из токена START_OBJECT

Я попытался добавить @JsonDeserialize(as=ItemList.class), но это не помогло.

Как поступить?

Ответ 1

Очевидно, что это не работает, потому что Джексон использует стандартный десериализатор коллекции для типов коллекции Java, который ничего не знает о свойствах ItemList.

Можно заставить его работать, но не очень элегантно. Вам нужно настроить ObjectMapper, чтобы заменить десериализатор коллекции по умолчанию на десериализаторе bean, созданного вручную для соответствующего типа. Я написал пример, который делает это в BeanDeserializerModifier для всех классов, аннотированных с помощью специальной аннотации.

Обратите внимание, что мне нужно переопределить ObjectMapper, чтобы получить доступ к защищенному методу createDeserializationContext из ObjectMapper, чтобы создать правильный контекст десериализации, поскольку модификатор bean не имеет к нему доступа.

Вот код:

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

}

Ответ 2

Если вы считаете, что свойство item является корневым значением, вы можете изменить свой класс ItemList следующим образом, используя @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
    // ...
}

Если вы активируете функцию UNWRAP_ROOT_VALUE десериализации, все будет работать как ожидалось:

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

Сериализация работает одинаково хорошо, с включенной WRAP_ROOT_VALUE функцией сериализации:

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}]}

Это решение, очевидно, не будет достаточным, если ваш ItemList содержит дополнительные свойства (кроме фактического списка), которые также должны быть сериализованы/десериализованы.