Java Jackson 2.8.3 сериализующий список, содержащий абстрактные объекты с циклическими зависимостями

#java #jackson

#java #джексон

Вопрос:

Я пытаюсь сериализовать некоторые данные с помощью jackson, что довольно хорошо работает в большинстве случаев, но теперь у меня проблема со списком. Список имеет тип A, который является абстрактным классом и может содержать циклические зависимости. Я не могу понять, как сериализовать эту конструкцию с помощью jackson. Комбинация identityInformation и typeInformation, похоже, не работает должным образом. Ниже приведен Examplecode, который создает проблему, с которой я сталкиваюсь.

Я использую Jackson версии 2.8.3. Я что-то упускаю? Существует ли хорошее решение для сериализации и десериализации такого рода списков?

Заранее спасибо за любую помощь!


Код:

 import java.util.ArrayList;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.CollectionType;

public class JacksonTest {

    @JsonTypeInfo(
        use = JsonTypeInfo.Id.CLASS,
        include = JsonTypeInfo.As.PROPERTY,
        property = "@class")

    @JsonSubTypes({ @JsonSubTypes.Type(value = B.class) })
    public static abstract class A {
        public A member;
    }

    @JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "@id")
    public static class B extends A {
    }

    public static void main(String[] args) {
        List<A> list = new ArrayList<A>();
        ObjectMapper mapper = new ObjectMapper();

        B instance1 = new B();
        B instance2 = new B();
        instance1.member = instance2;

        list.add(instance1);
        list.add(instance2);

        CollectionType listType = mapper.getTypeFactory().constructCollectionType(list.getClass(), A.class);

        try{
            String serialized = mapper.writerFor(listType).writeValueAsString(list);
            System.out.println(serialized);
            list = mapper.readValue(serialized, listType);          
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}
  

Сгенерированная строка json:

 [{"@class":"JacksonTest$B","@id":1,"member":{"@class":"JacksonTest$B","@id":2,"member":null}},2]
  

Ошибка при попытке прочитать строку:

 com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (VALUE_NUMBER_INT), expected FIELD_NAME: missing property '@class' that is to contain type id  (for class JacksonTest$A)
  

Похоже, Джексон ожидает, что вторая запись также будет объектом json, содержащим поле @class, и не распознает, что второй элемент массива является ссылкой на уже существующий объект.

Ответ №1:

Тип поля «A.member» — «A», и вы пропустили @JsonIdentityInfo для типа «A». Из-за этого Джексон считает объект из списка обычным объектом (был сериализован только идентификатор), а из-за отсутствия identityinfo Джексон не может применить механизм поиска ссылок на объекты во время десериализации, и, следовательно, вы видите ошибку.

Чтобы устранить вашу проблему, вам также нужно добавить «@JsonIdentityInfo для типа «A», или, что еще лучше, вы можете переместить объявление @JsonIdentityInfo из типа «B» в «A».

код:

 import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.junit.Assert.*;

import java.util.ArrayList;
import java.util.List;

import org.hamcrest.core.IsSame;
import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.CollectionType;

public class JacksonTest {

    @JsonTypeInfo(
                  use = JsonTypeInfo.Id.CLASS,
                  include = JsonTypeInfo.As.PROPERTY,
                  property = "@class")

    @JsonSubTypes({ @JsonSubTypes.Type(value = B.class) })
    @JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "@id")
    public static abstract class A {
        public A member;
    }

    public static class B extends A {
    }

    @Test
    public void testCirularPolymorphicSerialization() throws Exception {
        ObjectMapper mapper = new ObjectMapper();

        // set up fixture
        List<A> list = new ArrayList<A>();
        B instance1 = new B();
        B instance2 = new B();
        instance1.member = instance2;
        list.add(instance1);
        list.add(instance2);

        CollectionType listType = mapper.getTypeFactory().constructCollectionType(list.getClass(), A.class);
        String serialized = mapper.writerFor(listType).writeValueAsString(list);
        list = mapper.readValue(serialized, listType);

        assertThat(list.size(), is(2));
        assertThat(list.get(0).member, sameInstance(list.get(1)));
    }
}