Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 553
David Corsalini Avatar asked Jan 09 '23 22:01

David Corsalini


1 Answers

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;
}
like image 176
colriot Avatar answered Jan 18 '23 09:01

colriot