GSON serializuje listę obiektów polimorficznych

Próbuję serializować / deserializować obiekt, który wymaga polimorfizmu, do JSON za pomocą Gson.

To jest mój kod do serializacji:

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

Oto wynik:

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

Serializacja działa głównie, z wyjątkiem braku zawartości dziedziczonych elementów (w szczególności obix:BatchIn i obixBatchout brakuje łańcuchów). Oto moja podstawowa klasa:

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

Oto jak wygląda moja odziedziczona Klasa (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;
    }
}

Zdaję sobie sprawę, że przydałby mi się do tego adapter, ale problem w tym, że serializuję zbiór bazowego typu klasy ObixBaseObj. Istnieje około 25 klas, które dziedziczą z tego. Jak sprawić, by to działało elegancko?

Author: Elhanan Mishraky, 2013-10-25

3 answers

Myślę, że Niestandardowy serializer / deserializer jest jedynym sposobem na kontynuowanie i próbowałem zaproponować Ci najbardziej kompaktowy sposób realizacji tego, jaki znalazłem. Przepraszam, że nie korzystam z twoich zajęć, ale pomysł jest taki sam (chciałem tylko co najmniej 1 klasę bazową i 2 Rozszerzone klas).

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

A teraz jest to kod, który wykonałem, aby przetestować całość:

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

}

To moje wykonanie:

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]

Kilka wyjaśnień: sztuczka jest wykonywana przez inny Gson wewnątrz serializera / deserializera. I użyj tylko isA pola, aby znaleźć właściwą klasę. Aby działać szybciej, używam mapy do przypisania isA string do odpowiedniej klasy. Następnie wykonuję właściwą serializację / deserializację przy użyciu drugiego obiektu Gson. Zadeklarowałem to jako statyczne, więc nie będziesz spowalniać serializacji/deserializacji przy wielokrotnym Przydzielaniu Gson.

Pro Właściwie nie piszesz kodu więcej kodu niż ten, pozwalasz Gsonowi wykonywać całą pracę. Trzeba tylko pamiętać, aby umieścić nową podklasę na mapach (the wyjątek przypomina ci o tym).

Cons Masz dwie mapy. Myślę, że moja implementacja może nieco Dopracować, aby uniknąć powielania map, ale zostawiłem je tobie(lub przyszłemu edytorowi, jeśli w ogóle).

Może chcesz ujednolicić serializację i deserializację w unikalny obiekt, powinieneś sprawdzić klasę TypeAdapter lub poeksperymentować z obiektem, który implementuje oba interfejsy.

 34
Author: giampaolo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-08 10:24:24

Jest proste rozwiązanie: Gson ' s RuntimeTypeAdapterFactory . Nie musisz pisać żadnego serializera, ta klasa działa za Ciebie. Spróbuj tego z kodem:

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

}

Wyjście:

{"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: lepszy przykład pracy.

Powiedziałeś, że jest około 25 klas, które dziedziczą po ObixBaseObj.

Zaczynamy pisać nową klasę, 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();
    }

Za każdym razem, gdy potrzebujemy Gson obiektu, zamiast wywoływać new Gson(), zadzwonimy

GsonUtils.getGson()

Dodajemy ten kod do 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";
    }
Dlaczego? ponieważ za każdym razem, gdy ta klasa lub klasa dzieci ObixBaseObj jest tworzona, klasa będzie zarejestrowana w RuntimeTypeAdapter

W klasach dziecięcych potrzebna jest tylko minimalna zmiana:

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

Przykład roboczy:

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

    }

Wyjście:

{
  "type": "ObixBaseObj",
  "obix": "obj",
  "is": "obix:Lobby",
  "children": [
    {
      "type": "ObixOp",
      "in": "obix:BatchIn",
      "out": "obix:BatchOut",
      "obix": "op",
      "name": "batch",
      "children": []
    }
  ]
}
Mam nadzieję, że to pomoże.
 43
Author: rpax,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-11 08:03:25

Doceniam inne odpowiedzi tutaj, które doprowadziły mnie na mojej drodze do rozwiązania tego problemu. Użyłem kombinacji RuntimeTypeAdapterFactory z odbiciem .

Stworzyłem również klasę pomocniczą, aby upewnić się, że został użyty poprawnie skonfigurowany Gson.

Wewnątrz statycznego bloku wewnątrz klasy GsonHelper, mam następujący kod przejść przez mój projekt, aby znaleźć i zarejestrować wszystkie odpowiednie typy. Wszystkie moje obiekty, które przejdą przez JSON-iFIX są podtypem Jsonable. Będziesz chciał aby zmienić:

  1. mój.projekt w Reflections powinien być nazwą Twojego pakietu.
  2. To moja podstawowa klasa. Zastąp swoją.
  3. Lubię, gdy pole pokazuje pełną nazwę kanoniczną, ale oczywiście, jeśli nie chcesz / nie potrzebujesz, możesz pominąć tę część wywołania, aby zarejestrować Podtyp. To samo dotyczy className w RuntimeAdapterFactory; Mam już pozycje danych w polu 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();
    }
    
 3
Author: Jeff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-10-19 01:34:38