2017-01-04 5 views
3

Я пытаюсь десериализовать ответ с помощью Gson. Данные состоят из списков узлов, которые могут быть вложены в произвольные глубины. Выглядит JSON-то вроде этого:Gson: Как обрабатывать поле, которое может иметь другой тип?

{ 
    "type": "node", 
    "children": [ 
     { 
      "id": "abc123", 
      "name": "Name 1", 
      "subdata": { 
       "type": "node", 
       "children": [ 
        { 
         "id": "def456", 
         "name": "Name 2" 
        } 
       ] 
      } 
     } 
    ] 
} 

Теперь, без каких-либо адаптеров пользовательского типа, я могу сделать эту работу со следующими классами:

public class Data { 
    private String type; 
    private List<Node> nodes; 
} 

public class Node { 
    private String id; 
    private String name; 
    private Data subdata; 
} 

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

"subdata": { 
    "type": "extra", 
    "children": ["ghi", "jkl", "mno"] 
} 

Это, конечно, может быть представлен как класс Java, как это:

public class ExtraData { 
    private String type; 
    private List<String> children; 
} 

Вопрос в том, что: Как обрабатывать десериализацию так, чтобы subdata мог быть либо Data, либо ExtraData?

+0

I что-то подобное с Джексоном некоторое время назад: http://stackoverflow.com/a/12459070/823393 – OldCurmudgeon

ответ

1

Дети данных узлов всегда кажутся массивами JSON, поэтому первое, что вы могли с ними сделать, - объявить детям как List<?>, скрывающим фактический тип. Однако у вас все еще есть свойство/поле type, которое идеально подходит для получения фактического типа детей. Самый простой способ - это просто добавить еще один десериализатор JSON для десериализации Data экземпляров с некоторыми издержками производительности (поскольку они не являются адаптерами типа), и, насколько мне известно, отсутствует @SerializedName в полях класса Data.

Если вы также хорошо с изменив типы DTOS, предпочитаете перечисления, а не сырые нитей, как они работают просто отлично с перечислениями (особенно в сотрудничестве с интеллектуальной Идой):

enum Type { 

    @SerializedName("node") 
    NODE, 

    @SerializedName("extra") 
    EXTRA 

} 

Data самого классом, то может выглядеть следующим образом:

final class Data { 

    private final Type type; 
    private final List<?> children; // this one is supposed to be: 
            // * either List<String> if type=EXTRA 
            // * or List<Node> if type=NODE 

    Data(final Type type, final List<?> children) { 
     this.type = type; 
     this.children = children; 
    } 

    Type getType() { 
     return type; 
    } 

    List<?> getChildren() { 
     return children; 
    } 

} 

extra поскольку -typed дети просто строки в вашем вопросе, просто добавьте класс узла DTO:

final class Node { 

    @SerializedName("id") 
    private final String id = null; 

    @SerializedName("name") 
    private final String name = null; 

    @SerializedName("subdata") 
    private final Data subdata = null; 

    String getId() { 
     return id; 
    } 

    String getName() { 
     return name; 
    } 

    Data getSubdata() { 
     return subdata; 
    } 

} 

Теперь, десериализируя класс Data, вы можете определить фактический тип списка детей и, в соответствии с типом узла, десериализовать его как список строк или список узлов. Обратите внимание, что десериализатор ниже использует java.lang.reflect.Type экземпляры, а не java.lang.Class, потому что последний слаб из-за стирания стилей Java-типа и составляет List.class для любой параметризации списка (строки, узлы и т. Д.). Имея ожидаемые типы, снабженные токенами типа, просто делегируйте пару ключей/значений JSON в контекст десериализации, задающий целевой тип, тем самым делая рекурсивную десериализацию, которая будет работать на уровне произвольных вложенных элементов (однако GSON имеет некоторый внутренний предел стека, который ограничен 32 если я не ошибаюсь).

final class DataJsonDeserializer 
     implements JsonDeserializer<Data> { 

    private static final JsonDeserializer<Data> dataJsonDeserializer = new DataJsonDeserializer(); 

    private static final java.lang.reflect.Type nodeListType = new TypeToken<List<Node>>() { 
    }.getType(); 

    private static final java.lang.reflect.Type stringListType = new TypeToken<List<String>>() { 
    }.getType(); 

    private DataJsonDeserializer() { 
    } 

    static JsonDeserializer<Data> getDataJsonDeserializer() { 
     return dataJsonDeserializer; 
    } 

    @Override 
    public Data deserialize(final JsonElement jsonElement, final java.lang.reflect.Type type, final JsonDeserializationContext context) 
      throws JsonParseException { 
     final JsonObject rootJsonObject = jsonElement.getAsJsonObject(); 
     final Type nodeType = context.deserialize(rootJsonObject.get("type"), Type.class); 
     final JsonArray childrenJsonArray = rootJsonObject.get("children").getAsJsonArray(); 
     final List<?> children; 
     switch (nodeType) { 
     case NODE: 
      children = context.deserialize(childrenJsonArray, nodeListType); 
      break; 
     case EXTRA: 
      children = context.deserialize(childrenJsonArray, stringListType); 
      break; 
     default: 
      throw new AssertionError(nodeType); 
     } 
     return new Data(nodeType, children); 
    } 

} 

И демо рекурсивно проходит через детей (обратите внимание на расширенные for заявления, которые бросают каждый элемент к целевому типу ниже):

public final class EntryPoint { 

    private static final String JSON_WITH_SUBNODES = "{\"type\":\"node\",\"children\":[{\"id\":\"abc123\",\"name\":\"Name 1\",\"subdata\":{\"type\":\"node\",\"children\":[{\"id\":\"def456\",\"name\":\"Name 2\"}]}}]}"; 
    private static final String JSON_WITH_REFERENCES = "{\"type\":\"node\",\"children\":[{\"id\":\"abc123\",\"name\":\"Name 1\",\"subdata\":{\"type\":\"extra\",\"children\":[\"ghi\",\"jkl\",\"mno\"]}}]}"; 

    private static final Gson gson = new GsonBuilder() 
      .registerTypeAdapter(Data.class, getDataJsonDeserializer()) 
      .create(); 

    public static void main(final String... args) { 
     process(gson.fromJson(JSON_WITH_SUBNODES, Data.class)); 
     process(gson.fromJson(JSON_WITH_REFERENCES, Data.class)); 
    } 

    private static void process(final Data data) { 
     process(data, 0); 
     out.println(); 
    } 

    private static void process(final Data data, final int level) { 
     for (int i = 0; i < level; i++) { 
      out.print('>'); 
     } 
     final List<?> children = data.getChildren(); 
     final Type type = data.getType(); 
     out.println(type); 
     switch (type) { 
     case NODE: 
      @SuppressWarnings("unchecked") 
      final Iterable<Node> nodeChildren = (Iterable<Node>) children; 
      for (final Node node : nodeChildren) { 
       out.printf("\t%s %s\n", node.getId(), node.getName()); 
       final Data subdata = node.getSubdata(); 
       if (subdata != null) { 
        process(subdata, level + 1); 
       } 
      } 
      break; 
     case EXTRA: 
      @SuppressWarnings("unchecked") 
      final Iterable<String> extraChildren = (Iterable<String>) children; 
      for (final String extra : extraChildren) { 
       out.printf("\t%s\n", extra); 
      } 
      break; 
     default: 
      throw new AssertionError(type); 
     } 
    } 

} 

Выход:

NODE 
    abc123 Name 1 
>NODE 
    def456 Name 2 

NODE 
    abc123 Name 1 
>EXTRA 
    ghi 
    jkl 
    mno 
+0

Большое спасибо! Кажется, это именно то, что я искал. – manabreak

+0

@manabreak Добро пожаловать! :) –