David Corsalini
David Corsalini

Reputation: 8208

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 POJOs, 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 JSONObjects 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 APIs 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.

Upvotes: 4

Views: 1747

Answers (3)

Karrde
Karrde

Reputation: 471

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

Upvotes: 0

colriot
colriot

Reputation: 1988

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

Upvotes: 9

SnyersK
SnyersK

Reputation: 1296

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 e.g.

public class EventResponse {
    List<Event> events;
}

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

Upvotes: 0

Related Questions