Как решить циклическую ссылку при сериализации объекта, у которого есть член класса с тем же типом этого объекта

Я сталкиваюсь с этой проблемой при использовании Gson для сериализации объекта, который имеет член класса того же типа:

https://github.com/google/gson/issues/1447

Объект:

public class StructId implements Serializable {
private static final long serialVersionUID = 1L;

public String Name;
public StructType Type;
public StructId ParentId;
public StructId ChildId;

И поскольку StructId содержит ParentId/ChildId того же типа, я получал бесконечный цикл при попытке сериализовать его, поэтому я сделал следующее:

private Gson gson = new GsonBuilder()
.setExclusionStrategies(new ExclusionStrategy() {

        public boolean shouldSkipClass(Class<?> clazz) {
            return false; //(clazz == StructId.class);
        }

        /**
          * Custom field exclusion goes here
          */
        public boolean shouldSkipField(FieldAttributes f) {
            //Ignore inner StructIds to solve circular serialization
            return ( f.getName().equals("ParentId") || f.getName().equals("ChildId") ); 
        }

     })
    /**
      * Use serializeNulls method if you want To serialize null values 
      * By default, Gson does not serialize null values
      */
    .serializeNulls()
    .create();

Но этого недостаточно, потому что мне нужны данные внутри Parent/Child, и игнорирование их при сериализации не является решением. Как это возможно решить?

Связано с ответом, помеченным как Решение:

У меня есть такая структура: - Struct1 -- Table --- Variable1

Объект перед сериализацией: введите здесь описание изображения

И сгенерированный Json: введите здесь описание изображения

Как видите, ParentId таблицы — «Struct1», но ChildId «Struct1» пуст и должен быть «Table».

B.R.


person zbeedatm    schedule 02.01.2019    source источник
comment
Пожалуйста, отредактируйте свой вопрос, чтобы он содержал ваш фактический вопрос.   -  person ruakh    schedule 02.01.2019


Ответы (3)


Я думаю, что использование ExclusionStrategy не является правильным подходом к решению этой проблемы.

Я бы предпочел использовать JsonSerializer и JsonDeserializer настроен для вашего StructId класса.
(май подход с использованием TypeAdapter был бы еще лучше, но у меня не было достаточно опыта Gson, чтобы заставить это работать.)

Таким образом, вы должны создать свой экземпляр Gson следующим образом:

Gson gson = new GsonBuilder()
    .registerTypeAdapter(StructId.class, new StructIdSerializer())
    .registerTypeAdapter(StructId.class, new StructIdDeserializer())
    .setPrettyPrinting()
    .create();

Приведенный ниже класс StructIdSerializer отвечает за преобразование StructId в JSON. Он преобразует свои свойства Name, Type и ChildId в JSON. Обратите внимание, что он не преобразует свойство ParentId в JSON, потому что это приведет к бесконечной рекурсии.

public class StructIdSerializer implements JsonSerializer<StructId> {

    @Override
    public JsonElement serialize(StructId src, Type typeOfSrc, JsonSerializationContext context) {
        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("Name", src.Name);
        jsonObject.add("Type", context.serialize(src.Type));
        jsonObject.add("ChildId", context.serialize(src.ChildId));  // recursion!
        return jsonObject;
    }
}

Приведенный ниже класс StructIdDeserializer отвечает за преобразование JSON в StructId. Он преобразует свойства JSON Name, Type и ChildId в соответствующие поля Java в StructId. Обратите внимание, что поле ParentId Java реконструируется из вложенной структуры JSON, поскольку оно не содержится напрямую как свойство JSON.

public class StructIdDeserializer implements JsonDeserializer<StructId> {

    @Override
    public StructId deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
        throws JsonParseException {
        StructId id = new StructId();
        id.Name = json.getAsJsonObject().get("Name").getAsString();
        id.Type = context.deserialize(json.getAsJsonObject().get("Type"), StructType.class);
        JsonElement childJson = json.getAsJsonObject().get("ChildId");
        if (childJson != null) {
            id.ChildId = context.deserialize(childJson, StructId.class);  // recursion!
            id.ChildId.ParentId = id;
        }
        return id;
    }
}

Я протестировал приведенный выше код с помощью этого примера ввода JSON.

{
    "Name": "John",
    "Type": "A",
    "ChildId": {
        "Name": "Jane",
        "Type": "B",
        "ChildId": {
            "Name": "Joe",
            "Type": "A"
        }
    }
}

десериализовав его с помощью
StructId root = gson.fromJson(new FileReader("example.json"), StructId.class);,
затем сериализовав его с помощью
System.out.println(gson.toJson(root));
и снова получив исходный JSON.

person Thomas Fritsch    schedule 02.01.2019
comment
Звучит неплохо. Я реализовал решение Serializer/Deserializer, и у меня есть несколько вопросов: 1) почему вы использовали addProperty для имени, а не добавляли в качестве значения? 2) json, поступающий на контроллер сервера, выглядит следующим образом: {++\Name\:+\struct\} и при вызове id.Name = json.getAsJsonObject().get(Name).getAsString(); получение нуля - person zbeedatm; 02.01.2019
comment
@zbeedatm 1) Я использовал addProperty("Name", ...), потому что этого достаточно, чтобы добавить примитив JSON. 2) Этот JSON выглядит странно. Было бы лучше добавить отформатированный пример JSON к вашему вопросу, а не втискивать его в комментарий. Я также добавил в ответ свой пример ввода JSON. - person Thomas Fritsch; 02.01.2019
comment
Эти символы появляются из-за: URLEncoder.encode(jsonDesttinationIdString, ENCODING). Но он работал, как и ожидалось, до использования настроенного сериализатора... возможно, он найдет способ справиться с ним с другой стороны с помощью Encoder.decode. - person zbeedatm; 02.01.2019
comment
Хорошо, в основном, кажется, работает. Но без ParentId и эти данные тоже нужны... так что вы предлагаете? попробовать другой способ TypeAdapter? может быть, это достижимо там - person zbeedatm; 02.01.2019
comment
@zbeedatm Для меня ParentId был правильно установлен в объектах Java, а в JSON он не нужен. Я предлагаю вам придерживаться подхода сериализатора/десериализатора и заставить его работать. С подходом TypeAdapter вы получите ту же функциональность, только с лучшей производительностью в случае огромного содержимого JSON. - person Thomas Fritsch; 02.01.2019
comment
Большое спасибо @thomas-fritsch, в конце я использовал подход Adapter. - person zbeedatm; 03.01.2019

Просто чтобы показать один из способов сериализации (поэтому я не занимаюсь десериализацией) с помощью TypeAdapter и ExclusionStrategy. Возможно, это не самая красивая реализация, но в любом случае она достаточно универсальна.

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

Сначала нам нужны настраиваемые ExclusionStrategy, такие как:

public class FieldExclusionStrategy implements ExclusionStrategy {

    private final List<String> skipFields;

    public FieldExclusionStrategy(String... fieldNames) {
        skipFields = Arrays.asList(fieldNames);
    }

    @Override
    public boolean shouldSkipField(FieldAttributes f) {
        return skipFields.contains(f.getName());
    }

    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
        return false;
    }

}

Тогда TypeAdapter будет выглядеть так:

public class LinkedListAdapter extends TypeAdapter<StructId> {

private static final String PARENT_ID = "ParentId";
private static final String CHILD_ID = "ChildId";
private Gson gson;

@Override
public void write(JsonWriter out, StructId value) throws IOException {
    // First serialize everything but StructIds
    // You could also use type based exclusion strategy
    // but for brevity I use just this one  
    gson = new GsonBuilder()
            .addSerializationExclusionStrategy(
                    new FieldExclusionStrategy(CHILD_ID, PARENT_ID))
            .create();
    JsonObject structObject = gson.toJsonTree(value).getAsJsonObject(); 
    JsonObject structParentObject;
    JsonObject structChildObject;

    // If exists go through the ParentId side in one direction.
    if(null!=value.ParentId) {
        gson = new GsonBuilder()
                .addSerializationExclusionStrategy(new FieldExclusionStrategy(CHILD_ID))
                .create();
        structObject.add(PARENT_ID, gson.toJsonTree(value.ParentId));

        if(null!=value.ParentId.ChildId) {
            gson = new GsonBuilder()
                    .addSerializationExclusionStrategy(new FieldExclusionStrategy(PARENT_ID))
                    .create();
            structParentObject = structObject.get(PARENT_ID).getAsJsonObject();
            structParentObject.add(CHILD_ID, gson.toJsonTree(value.ParentId.ChildId).getAsJsonObject());
        }
    }
    // And also if exists go through the ChildId side in one direction.
    if(null!=value.ChildId) {
        gson = new GsonBuilder()
                .addSerializationExclusionStrategy(new FieldExclusionStrategy(PARENT_ID))
                .create();
        structObject.add(CHILD_ID, gson.toJsonTree(value.ChildId));

        if(null!=value.ChildId.ParentId) {
            gson = new GsonBuilder()
                    .addSerializationExclusionStrategy(new FieldExclusionStrategy(CHILD_ID))
                    .create();

            structChildObject = structObject.get(CHILD_ID).getAsJsonObject();
            structChildObject.add(PARENT_ID, gson.toJsonTree(value.ChildId.ParentId).getAsJsonObject());
        }
    }

    // Finally write the combined result out. No need to initialize gson anymore
    // since just writing JsonElement
    gson.toJson(structObject, out);
}

@Override
public StructId read(JsonReader in) throws IOException {
    return null;
}}

Тестирование:

@Slf4j
public class TestIt extends BaseGsonTest {

@Test
public void test1() {
    StructId grandParent   = new StructId();

    StructId parent   = new StructId();
    grandParent.ChildId = parent;
    parent.ParentId = grandParent;

    StructId child = new StructId();
    parent.ChildId = child;
    child.ParentId = parent;

    Gson gson = new GsonBuilder()
            .setPrettyPrinting()
            .registerTypeAdapter(StructId.class, new LinkedListAdapter())
            .create();

    log.info("\n{}", gson.toJson(parent));
}}

Дал бы вам что-то вроде:

{
  "Name": "name1237598030",
  "Type": {
       "name": "name688766789"
   },
  "ParentId": {
  "Name": "name1169146729",
  "Type": {
     "name": "name2040352617"
  }
 },
"ChildId": {
  "Name": "name302155142",
  "Type": {
     "name": "name24606376"
   }
 }
}

Имена в моем тестовом материале просто по умолчанию инициализируются с помощью "name"+hashCode()

person pirho    schedule 02.01.2019
comment
Привет еще раз, я понял, что 2 уровня недостаточно... Мне нужно заполнить все дерево структуры до последнего узла - person zbeedatm; 03.01.2019
comment
Вы пробовали? Думаю должно работать. Единственное, что корневой узел всегда является узлом, который вы даете, и тогда он будет иметь родительское и дочернее дерево. - person pirho; 03.01.2019
comment
Я отредактировал свой пост, добавив дополнительную информацию о вашем решении, пожалуйста, посмотрите. - person zbeedatm; 03.01.2019
comment
Можно ли ограничить его только 2-3 уровнями и предотвратить бесконечный цикл? Я думаю, будет достаточно, если каждый объект может содержать полностью заполненные родительские и дочерние объекты. - person zbeedatm; 03.01.2019
comment
Если я правильно понял ваши изменения, вы хотите снова реплицировать StructId=59 внутри его ParentId, StructId=68 (но без его ParentId, structId=68 снова, чтобы предотвратить rtecursion). Но почему? У вас уже есть данные в качестве корневого элемента. Может быть, вам следует переосмыслить свою настоящую проблему и, возможно, переформулировать свой вопрос или задать другой вопрос? - person pirho; 03.01.2019
comment
Да, мне нужны эти данные (59, как ребенок из 68). У меня есть тест Junit, который проходит с нормальным потоком, и при запуске того же теста через остальные API и работе с сериализацией объектов он терпит неудачу. Я снова отредактировал свой пост и дал логику, в которой она терпит неудачу, если это помогает понять. Он терпит неудачу, когда входит в третье if и снова вызывает сам метод -recursion- со структуройIterator, у которой нет дочерних элементов. - person zbeedatm; 04.01.2019
comment
Я отредактировал ваш LinkedListAdapter, чтобы добавить больше вложенных уровней, и я добился прогресса. Отмечу это как решение, так как пока у меня нет другого способа справиться с этим, надеюсь, не возникнет новых проблем, пока я продолжу свои тесты. - person zbeedatm; 06.01.2019
comment
@zbeedatm Как хотите, но я все еще думаю, что могло бы быть еще лучшее решение, если бы реальная проблема была лучше известна. Я вижу ваш тестовый пример, но не уверен, почему вам нужно делать это именно так. Может быть, я расследую это позже. - person pirho; 06.01.2019

Извините, что ввел вас в заблуждение, ребята, на основании этого поста:

Есть ли решение для циклической ссылки Gson?

«в Gson нет автоматизированного решения для циклических ссылок. Единственная известная мне библиотека для создания JSON, которая автоматически обрабатывает циклические ссылки, — это XStream (с серверной частью Jettison)".

Но это в том случае, если вы не используете Джексона! Если вы уже используете Jackson для создания контроллеров REST API, почему бы не использовать его для сериализации. Нет необходимости во внешних компонентах, таких как: Gson или XStream.

Решение с Джексоном:

Сериализация:

ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
    try {
        jsonDesttinationIdString = ow.writeValueAsString(destinationId);
    } catch (JsonProcessingException ex) {
        throw new SpecificationException(ex.getMessage());
    }

Десериализация:

ObjectMapper mapper = new ObjectMapper();
    try {
        destinationStructId = destinationId.isEmpty() ? null : mapper.readValue(URLDecoder.decode(destinationId, ENCODING), StructId.class);
    } catch (IOException e) {
        throw new SpecificationException(e.getMessage());
    }

И самое главное, вы должны использовать аннотацию @JsonIdentityInfo:

//@JsonIdentityInfo(
//        generator = ObjectIdGenerators.PropertyGenerator.class, 
//        property = "Name")
@JsonIdentityInfo(
      generator = ObjectIdGenerators.UUIDGenerator.class, 
      property = "id")
public class StructId implements Serializable {
    private static final long serialVersionUID = 1L;

    @JsonProperty("id") // I added this field to have a unique identfier
    private UUID id = UUID.randomUUID();
person zbeedatm    schedule 08.01.2019