2013-10-25 6 views
34

Я пытаюсь сериализовать/десериализовать объект, который включает полиморфизм, в JSON с использованием Gson.Gson сериализует список полиморфных объектов

Это мой код сериализации:

ObixBaseObj lobbyObj = new ObixBaseObj(); 
lobbyObj.setIs("obix:Lobby"); 

ObixOp batchOp = new ObixOp(); 
batchOp.setName("batch"); 
batchOp.setIn("obix:BatchIn"); 
batchOp.setOut("obix:BatchOut"); 

lobbyObj.addChild(batchOp); 

Gson gson = new Gson(); 
System.out.println(gson.toJson(lobbyObj)); 

Вот результат:

{"obix":"obj","is":"obix:Lobby","children":[{"obix":"op","name":"batch"}]} 

Сериализация в основном работает, за исключением его отсутствующего содержания унаследованных членов (в частности obix:BatchIn и obixBatchout струн отсутствует). Вот мой базовый класс:

public class ObixBaseObj { 
    protected String obix; 
    private String display; 
    private String displayName; 
    private ArrayList<ObixBaseObj> children; 

    public ObixBaseObj() 
    { 
     obix = "obj"; 
    } 

    public void setName(String name) { 
     this.name = name; 
    } 
     ... 
} 

Вот что выглядит мой наследуется класс (ObixOp), как:

public class ObixOp extends ObixBaseObj { 
    private String in; 
    private String out; 

    public ObixOp() { 
     obix = "op"; 
    } 
    public ObixOp(String in, String out) { 
     obix = "op"; 
     this.in = in; 
     this.out = out; 
    } 
    public String getIn() { 
     return in; 
    } 
    public void setIn(String in) { 
     this.in = in; 
    } 
    public String getOut() { 
     return out; 
    } 
    public void setOut(String out) { 
     this.out = out; 
    } 
} 

Я понимаю, что я мог бы использовать адаптер для этого, но проблема в том, что я сериализации набор базового класса ObixBaseObj. Наследуется от этого 25 классов. Как я могу сделать эту работу элегантно?

ответ

30

Я думаю, что пользовательский сериализатор/десериализатор - единственный способ продолжения, и я попытался предложить вам самый компактный способ реализовать его, который я нашел. Я извиняюсь за то, что не использовал ваши классы, но идея такая же (мне просто нужен как минимум 1 базовый класс и 2 расширенных класса).

BaseClass.java

public class BaseClass{ 

    @Override 
    public String toString() { 
     return "BaseClass [list=" + list + ", isA=" + isA + ", x=" + x + "]"; 
    } 

    public ArrayList<BaseClass> list = new ArrayList<BaseClass>(); 

    protected String isA="BaseClass"; 
    public int x; 

} 

ExtendedClass1.java

public class ExtendedClass1 extends BaseClass{ 

    @Override 
    public String toString() { 
     return "ExtendedClass1 [total=" + total + ", number=" + number 
      + ", list=" + list + ", isA=" + isA + ", x=" + x + "]"; 
    } 

    public ExtendedClass1(){ 
     isA = "ExtendedClass1"; 
    } 

    public Long total; 
    public Long number; 

} 

ExtendedClass2.java

public class ExtendedClass2 extends BaseClass{ 

    @Override 
    public String toString() { 
     return "ExtendedClass2 [total=" + total + ", list=" + list + ", isA=" 
      + isA + ", x=" + x + "]"; 
    } 

    public ExtendedClass2(){ 
     isA = "ExtendedClass2"; 
    } 

    public Long total; 

} 

CustomDeserializer.java

public class CustomDeserializer implements JsonDeserializer<List<BaseClass>> { 

    private static Map<String, Class> map = new TreeMap<String, Class>(); 

    static { 
     map.put("BaseClass", BaseClass.class); 
     map.put("ExtendedClass1", ExtendedClass1.class); 
     map.put("ExtendedClass2", ExtendedClass2.class); 
    } 

    public List<BaseClass> deserialize(JsonElement json, Type typeOfT, 
      JsonDeserializationContext context) throws JsonParseException { 

     List list = new ArrayList<BaseClass>(); 
     JsonArray ja = json.getAsJsonArray(); 

     for (JsonElement je : ja) { 

      String type = je.getAsJsonObject().get("isA").getAsString(); 
      Class c = map.get(type); 
      if (c == null) 
       throw new RuntimeException("Unknow class: " + type); 
      list.add(context.deserialize(je, c)); 
     } 

     return list; 

    } 

} 

CustomSerializer.java

public class CustomSerializer implements JsonSerializer<ArrayList<BaseClass>> { 

    private static Map<String, Class> map = new TreeMap<String, Class>(); 

    static { 
     map.put("BaseClass", BaseClass.class); 
     map.put("ExtendedClass1", ExtendedClass1.class); 
     map.put("ExtendedClass2", ExtendedClass2.class); 
    } 

    @Override 
    public JsonElement serialize(ArrayList<BaseClass> src, Type typeOfSrc, 
      JsonSerializationContext context) { 
     if (src == null) 
      return null; 
     else { 
      JsonArray ja = new JsonArray(); 
      for (BaseClass bc : src) { 
       Class c = map.get(bc.isA); 
       if (c == null) 
        throw new RuntimeException("Unknow class: " + bc.isA); 
       ja.add(context.serialize(bc, c)); 

      } 
      return ja; 
     } 
    } 
} 

и теперь это код, я выполнил, чтобы проверить все это:

public static void main(String[] args) { 

    BaseClass c1 = new BaseClass(); 
    ExtendedClass1 e1 = new ExtendedClass1(); 
    e1.total = 100L; 
    e1.number = 5L; 
    ExtendedClass2 e2 = new ExtendedClass2(); 
    e2.total = 200L; 
    e2.x = 5; 
    BaseClass c2 = new BaseClass(); 

    c1.list.add(e1); 
    c1.list.add(e2); 
    c1.list.add(c2); 


    List<BaseClass> al = new ArrayList<BaseClass>(); 

    // this is the instance of BaseClass before serialization 
    System.out.println(c1); 

    GsonBuilder gb = new GsonBuilder(); 

    gb.registerTypeAdapter(al.getClass(), new CustomDeserializer()); 
    gb.registerTypeAdapter(al.getClass(), new CustomSerializer()); 
    Gson gson = gb.create(); 

    String json = gson.toJson(c1); 
    // this is the corresponding json 
    System.out.println(json); 

    BaseClass newC1 = gson.fromJson(json, BaseClass.class); 

    System.out.println(newC1); 

} 

Это мое исполнение :

BaseClass [list=[ExtendedClass1 [total=100, number=5, list=[], isA=ExtendedClass1, x=0], ExtendedClass2 [total=200, list=[], isA=ExtendedClass2, x=5], BaseClass [list=[], isA=BaseClass, x=0]], isA=BaseClass, x=0] 
{"list":[{"total":100,"number":5,"list":[],"isA":"ExtendedClass1","x":0},{"total":200,"list":[],"isA":"ExtendedClass2","x":5},{"list":[],"isA":"BaseClass","x":0}],"isA":"BaseClass","x":0} 
BaseClass [list=[ExtendedClass1 [total=100, number=5, list=[], isA=ExtendedClass1, x=0], ExtendedClass2 [total=200, list=[], isA=ExtendedClass2, x=5], BaseClass [list=[], isA=BaseClass, x=0]], isA=BaseClass, x=0] 

Некоторые объяснения: трюк выполняется другим Gson внутри сериализатора/десериализатора. Я использую только поле isA, чтобы найти подходящий класс. Чтобы идти быстрее, я использую карту, чтобы связать строку isA с соответствующим классом. Затем я делаю правильную сериализацию/десериализацию с использованием второго объекта Gson. Я объявил это как статический, поэтому вы не будете замедлять сериализацию/десериализацию с множественным распределением Gson.

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

Против У вас есть две карты. Я думаю, что моя реализация может немного уточнить, чтобы избежать дублирования карт, но я оставил их вам (или будущему редактору, если таковые имеются).

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

+0

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

+0

Чтобы быть более конкретным, если я добавлю в свой BaseClass частный элемент массива ArrayList , добавьте объекты, которые наследуются от BaseClass к этому списку (ExtendedClass1,2), а затем попытаются сериализовать единственный экземпляр BaseClass, он неправильно упорядочивает содержащий arraylist. – l46kok

+0

На самом деле, аналогичная проблема для десериализации тоже. Все становится десериализованным как базовый класс. – l46kok

39

Существует простое решение: Gson's RuntimeTypeAdapterFactory. Вам не нужно писать какой-либо сериализатор, этот класс все работает для вас. Попробуйте это с кодом:

ObixBaseObj lobbyObj = new ObixBaseObj(); 
    lobbyObj.setIs("obix:Lobby"); 

    ObixOp batchOp = new ObixOp(); 
    batchOp.setName("batch"); 
    batchOp.setIn("obix:BatchIn"); 
    batchOp.setOut("obix:BatchOut"); 

    lobbyObj.addChild(batchOp); 

    RuntimeTypeAdapterFactory<ObixBaseObj> adapter = 
        RuntimeTypeAdapterFactory 
        .of(ObixBaseObj.class) 
        .registerSubtype(ObixBaseObj.class) 
        .registerSubtype(ObixOp.class); 


    Gson gson2=new GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(adapter).create(); 
    Gson gson = new Gson(); 
    System.out.println(gson.toJson(lobbyObj)); 
    System.out.println("---------------------"); 
    System.out.println(gson2.toJson(lobbyObj)); 

} 

Выход:

{"obix":"obj","is":"obix:Lobby","children":[{"obix":"op","name":"batch","children":[]}]} 
--------------------- 
{ 
    "type": "ObixBaseObj", 
    "obix": "obj", 
    "is": "obix:Lobby", 
    "children": [ 
    { 
     "type": "ObixOp", 
     "in": "obix:BatchIn", 
     "out": "obix:BatchOut", 
     "obix": "op", 
     "name": "batch", 
     "children": [] 
    } 
    ] 
} 

EDIT: Лучше рабочий пример.

Вы сказали, что существует около 25 классов, которые наследуются от ObixBaseObj.

Мы начали писать новый класс, GsonUtils

public class GsonUtils { 

    private static final GsonBuilder gsonBuilder = new GsonBuilder() 
      .setPrettyPrinting(); 

    public static void registerType(
      RuntimeTypeAdapterFactory<?> adapter) { 
     gsonBuilder.registerTypeAdapterFactory(adapter); 
    } 

    public static Gson getGson() { 
     return gsonBuilder.create(); 
    } 

Каждый раз, когда нам нужен Gson объект, вместо вызова new Gson(), мы будем называть

GsonUtils.getGson() 

Мы добавляем этот код ObixBaseObj:

public class ObixBaseObj { 
    protected String obix; 
    private String display; 
    private String displayName; 
    private String name; 
    private String is; 
    private ArrayList<ObixBaseObj> children = new ArrayList<ObixBaseObj>(); 
    // new code 
    private static final RuntimeTypeAdapterFactory<ObixBaseObj> adapter = 
      RuntimeTypeAdapterFactory.of(ObixBaseObj.class); 

    private static final HashSet<Class<?>> registeredClasses= new HashSet<Class<?>>(); 

    static { 
     GsonUtils.registerType(adapter); 
    } 

    private synchronized void registerClass() { 
     if (!registeredClasses.contains(this.getClass())) { 
      registeredClasses.add(this.getClass()); 
      adapter.registerSubtype(this.getClass()); 
     } 
    } 
    public ObixBaseObj() { 
     registerClass(); 
     obix = "obj"; 
    } 

Почему? потому что каждый раз, когда этот класс или класс детей из ObixBaseObj конкретизируется, класса это собирается быть зарегистрирован в RuntimeTypeAdapter

В дочерних классах, требуется лишь минимальное изменение:

public class ObixOp extends ObixBaseObj { 
    private String in; 
    private String out; 

    public ObixOp() { 
     super(); 
     obix = "op"; 
    } 

    public ObixOp(String in, String out) { 
     super(); 
     obix = "op"; 
     this.in = in; 
     this.out = out; 
    } 

Рабочего пример :

public static void main(String[] args) { 

     ObixBaseObj lobbyObj = new ObixBaseObj(); 
     lobbyObj.setIs("obix:Lobby"); 

     ObixOp batchOp = new ObixOp(); 
     batchOp.setName("batch"); 
     batchOp.setIn("obix:BatchIn"); 
     batchOp.setOut("obix:BatchOut"); 

     lobbyObj.addChild(batchOp); 



     Gson gson = GsonUtils.getGson(); 
     System.out.println(gson.toJson(lobbyObj)); 

    } 

Выход:

{ 
    "type": "ObixBaseObj", 
    "obix": "obj", 
    "is": "obix:Lobby", 
    "children": [ 
    { 
     "type": "ObixOp", 
     "in": "obix:BatchIn", 
     "out": "obix:BatchOut", 
     "obix": "op", 
     "name": "batch", 
     "children": [] 
    } 
    ] 
} 

Надеюсь, это поможет.

+0

Извините, но в чем смысл поставить награду за принятый ответ? Конечно, вы примете свой собственный вопрос, поэтому я не пойму. – giampaolo

+0

Или создайте вместо этого страницу вики. – giampaolo

+0

Извините, я имел в виду дать щедрость. Полиморфная десериализация с Gson является довольно обсуждаемой проблемой, поэтому такой вопрос возникает часто. Я считаю, что лучшей стратегией является открытие вики. – giampaolo

3

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

Я также создал вспомогательный класс, чтобы убедиться, что был правильно настроен Gson.

Внутри статического блока внутри класса GsonHelper у меня есть следующий код, который проходит через мой проект, чтобы найти и зарегистрировать все соответствующие типы. Все мои объекты, которые пройдут через JSON-ification, являются подтипом Jsonable. Вы хотите изменить следующие параметры:

  1. my.project в Reflections должно быть ваше имя пакета.
  2. Jsonable.class - мой базовый класс. Замените свое.
  3. Мне нравится, когда поле показывает полное каноническое имя, но ясно, если вы его не хотите/нуждаетесь, вы можете оставить эту часть вызова для регистрации подтипа. То же самое касается className в RuntimeAdapterFactory; У меня есть элементы данных, которые уже используют поле type.

    private static final GsonBuilder gsonBuilder = new GsonBuilder() 
        .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") 
        .excludeFieldsWithoutExposeAnnotation() 
        .setPrettyPrinting(); 
    
    static { 
    Reflections reflections = new Reflections("my.project"); 
    
    Set<Class<? extends Jsonable>> allTypes = reflections.getSubTypesOf(Jsonable.class); 
    for (Class< ? extends Jsonable> serClass : allTypes){ 
        Set<?> subTypes = reflections.getSubTypesOf(serClass); 
        if (subTypes.size() > 0){ 
         RuntimeTypeAdapterFactory<?> adapterFactory = RuntimeTypeAdapterFactory.of(serClass, "className"); 
         for (Object o : subTypes){ 
          Class c = (Class)o; 
          adapterFactory.registerSubtype(c, c.getCanonicalName()); 
         } 
         gsonBuilder.registerTypeAdapterFactory(adapterFactory); 
        } 
    } 
    } 
    
    public static Gson getGson() { 
        return gsonBuilder.create(); 
    } 
    
+0

Хорошее решение. Он чище, чем мой. + 1 – rpax

Смежные вопросы