简体   繁体   中英

Retrofit, Gson and an array of heterogeneous objects

I'm using Retrofit to perform REST requests to our server. One of these requests return an array of objects that, once deserialized in POJO s, extend from the abstract class Event . Event has the method getEventType() that returns a String , this string is the value for the key "EventType" that I will always have inside the JSONObject s in the array.

This is how the JSON will look like (we have 7 type of objects as of now):

[
 {
  "EventType":"typeA",
  "Data":"data"
 }, 
 {
  "EventType":"typeB",
  "OtherData":3
 }
] 

I'm trying to use Retrofit and GSON API s to deserialize this JSON inside the async call, to use a Callback<List<Event>> as a parameter for the call, but I still can't find a way to do it.

You can write custom Gson TypeAdapterFactory for this case. The thing is to determine type of an event and then to use default TypeAdapter for that type. That's exactly what I've done:

public class EventTypeAdapterFactory implements TypeAdapterFactory {
    private static final String TAG = EventTypeAdapterFactory.class.getSimpleName();

    private Map<EventType, TypeAdapter<? extends Event>> ADAPTERS = new ArrayMap<>();
    private TypeAdapter<Event> baseTypeAdapter;
    private TypeAdapter<JsonElement> elementAdapter;
    private TypeAdapter<EventType> eventTypeAdapter;

    @Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
      if (!Event.class.isAssignableFrom(type.getRawType())) return null;

      ADAPTERS.put(EventType.TYPE_A, gson.getDelegateAdapter(this, TypeToken.get(TypeAEvent.class)));
      ADAPTERS.put(EventType.TYPE_B, gson.getDelegateAdapter(this, TypeToken.get(TypeBEvent.class)));

      baseTypeAdapter = gson.getDelegateAdapter(this, TypeToken.get(Event.class));

      elementAdapter = gson.getAdapter(JsonElement.class);
      eventTypeAdapter = gson.getAdapter(EventType.class);

      return (TypeAdapter<T>) new EventTypeAdapter().nullSafe();
    }

    private class EventTypeAdapter extends TypeAdapter<Event> {

      @Override public void write(JsonWriter out, Event value) throws IOException {
        EventType eventType = value.getType();
        TypeAdapter<? extends Event> adapter = eventType == null ? baseTypeAdapter : ADAPTERS.get(eventType);
        if (value instanceof TypeAEvent) {
          writeWrap(adapter, out, (TypeAEvent) value, TypeAEvent.class);
        } else if (value instanceof TypeBEvent) {
          writeWrap(adapter, out, (TypeBEvent) value, TypeBEvent.class);
        } else {
          writeWrap(adapter, out, value, Event.class);
        }
      }

      private <T extends Event> void writeWrap(TypeAdapter<? extends Event> adapter,
          JsonWriter out, T value, Class<T> dummyForT) throws IOException {
        ((TypeAdapter<T>)adapter).write(out, value);
      }

      @Override public Event read(JsonReader in) throws IOException {
        JsonObject objectJson = elementAdapter.read(in).getAsJsonObject();
        JsonElement typeJson = objectJson.get("EventType");

        EventType eventType = eventTypeAdapter.fromJsonTree(typeJson);

        if (eventType == null) {
          Log.w(TAG, "Unsupported EventType: " + typeJson);
        }

        TypeAdapter<? extends Event> adapter = eventType == null ? baseTypeAdapter : ADAPTERS.get(eventType);
        return adapter.fromJsonTree(objectJson);
      }
    }
  }

// EventType enum, change to reflect your values.
enum EventType {
    TYPE_A, TYPE_B; 
}

// Base Event type and its successors.
class Event {
    @SerializedName("EventType")
    private EventType type;

    public EventType getType() {
        return type;
    }
}

class TypeAEvent extends Event {
    @SerializedName("Data")
    public String data;
}

class TypeBEvent extends Event {
    @SerializedName("OtherData")
    public int otherData;
}

I'm not sure, since I have not tested this, but if you write a custom deserialiser like this:

private class MyEventDeserialiser implements JsonDeserializer<Event> {

    @Override
    public Event deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException {

        JsonArray jArray = (JsonArray) json;

        for (int i=1; i<jArray.size(); i++) {
            JsonObject obj = jArray.get(i).getAsJsonObject();
            String eventType = String.valueOf(obj.get("EventType"));

            //check here which type it is
            Event event = null;

            if(eventType.equals("TypeA")) {
                Event event = context.deserialize(obj, TypeA.class);    
            }
            ...

            return event;
        }
    }
}

then set this one on your Gson deserialiser that you use for Retrofit, it might work.

It might be possible that you have to encapsulate your List of Events in another class eg

public class EventResponse {
    List<Event> events;
}

and then use that in your interface as a parameter, but i'm not sure about that.

Very similar to colriot's answer. I've modified a bit so that I've embedded a class into the json, and only when it's serialised - that map is kinda ugly.

It's also not as robust since I'm happy to fail on nulls.

public final class ModelTypeAdapter extends TypeAdapter<Model> {

    private static final String MODEL_CLASS_PROPERTY_NAME = "modelClass";

    private final Gson gson;
    private final TypeAdapterFactory containerFactory;

    @SuppressWarnings("rawtypes")
    private final TypeAdapter<Class> classTypeAdapter;
    private final TypeAdapter<JsonElement> jsonElementAdapter;

    public ModelTypeAdapter(final Gson gson, final TypeAdapterFactory containerFactory) {
        this.gson = gson;
        this.containerFactory = containerFactory;

        this.classTypeAdapter = gson.getAdapter(Class.class);
        this.jsonElementAdapter = gson.getAdapter(JsonElement.class);
    }

    @Override
    public final void write(JsonWriter out, Model value) throws IOException {
        doWrite(out, value);
    }

    private final <M extends Model> void doWrite(JsonWriter out, M value) throws IOException {
        @SuppressWarnings("unchecked")
        final Class<M> modelClass = (Class<M>) value.getClass();

        final TypeAdapter<M> delegateAdapter = gson.getDelegateAdapter(containerFactory, TypeToken.get(modelClass));
        final JsonObject jsonObject = delegateAdapter.toJsonTree(value).getAsJsonObject();
        jsonObject.add(MODEL_CLASS_PROPERTY_NAME, classTypeAdapter.toJsonTree(modelClass));

        jsonElementAdapter.write(out, jsonObject);
    }

    @Override
    public final Model read(JsonReader in) throws IOException {
        final JsonObject jsonObject = jsonElementAdapter.read(in).getAsJsonObject();
        @SuppressWarnings("unchecked")
        final Class<? extends Model> modelClass = classTypeAdapter.fromJsonTree(jsonObject.get(MODEL_CLASS_PROPERTY_NAME));

        jsonObject.remove(MODEL_CLASS_PROPERTY_NAME);
        final TypeAdapter<? extends Model> delegateAdapter = gson.getDelegateAdapter(containerFactory, TypeToken.get(modelClass));

        return delegateAdapter.fromJsonTree(jsonObject);
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM