Gson using AutoValue and Polymorphism

Recently, I ran into a situation where a RESTful API provided me with a list of objects. Normally this isn’t a problem since REST provides you with a structure that is easy to convert. The catch was that some objects inside other objects were polymorphic and required to parse specific fields.

The previous solution was to have a HashMap that contained all the supplied fields. The application then needed to check what kind of object was returned and decide if all required key-value pairs were filled with usable data. This was not a preferred solution and prone to errors.

A lot of suggestions were made to look into custom (de)serializers, which is a good idea, but also a lot of work. I came across the “hidden” RuntimeTypeAdapterFactory class in the Gson repository which solved most of my problem quite nicely.

An Example Problem

Take the following JSON-structure for example:

{
  "article": {
    "title": "Hello world",
    "body": [{
        "type": "text",
        "text": "Hi there"
      }, {
        "type": "image",
        "source": "https://example.com/hello-world-banner.jpg",
        "author": "John Doe",
        "description": "Hello World! banner"
      }, {
        "type": "video",
        "source": "http://mirrorblender.top-ix.org/peach/bigbuckbunny_movies/big_buck_bunny_1080p_surround.avi",
        "thumbnail": "https://peach.blender.org/wp-content/uploads/dl_1080p.jpg",
        "title": "Big Buck Bunny",
        "duration": 574
      }
    ]
  }
}

As you can see, the objects in the body array contain a field called type, which determines the type of object being returned. This will give you flexibility to parse the objects into their own respected models, which in this case should be “text”, “image” and “video”. See the 3 model examples below using Google’s AutoValue.

public interface Block {
}

@AutoValue
public abstract class TextBlock implements Block {
  @SerializedName("text")
  public abstract String text();
}

@AutoValue
public abstract class ImageBlock implements Block {
  @SerializedName("source")
  public abstract String source();

  @Nullable
  @SerializedName("author")
  public abstract String author();

  @Nullable
  @SerializedName("description")
  public abstract String description();
}

@AutoValue
public abstract class VideoBlock implements Block {
  @SerializedName("source")
  public abstract String source();

  @SerializedName("thumbnail")
  public abstract String thumbnail();

  @SerializedName("title")
  public abstract String title();

  @SerializedName("duration")
  public abstract int duration();
}

(For brevity sake I left out the Builders and Gson Type Adapter generators in the above examples.)

The Solution

First we’ll need to create the Article model itself. In the example below you’ll notice the List<Block> return type. If you take a look at the Blocks example code, you’ll see these all implement the Block interface. This way we can easily define new typed objects we want to support in future updates.

@AutoValue
public abstract class Article {
  @SerializedName("title")
  public abstract String title();

  @SerializedName("blocks")
  public abstract List<Block> blocks();
}

But now parsing it; here the magic called RuntimeTypeAdapterFactory will step in! When creating the Gson parser, you can register all kinds of type adapter factories. Add the generated AutoValue adapter and the Runtime adapter and you’re set.

For example:

RuntimeTypeAdapterFactory<Block> articleBlockFactory = RuntimeTypeAdapterFactory.of(Block.class, "type")
  .registerSubtype(TextBlock.class, "text")
  .registerSubtype(ImageBlock.class, "image")
  .registerSubtype(VideoBlock.class, "video");

Gson gson = new GsonBuilder()
  .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
  .registerTypeAdapterFactory(AutoValueTypeAdapterFactory.create())
  .registerTypeAdapterFactory(articleBlockFactory)
  .create();

Retrofit retrofit = new Retrofit.Builder()
  .baseUrl("https://api.example.com/")
  .addConverterFactory(GsonConverterFactory.create(gson))
  .build();

Whenever the Gson parser encounters an object of the type Block the adapter will check if it can parse to any of the defined subtypes. If your API defines the type in a different field than "type" you can provide it. "type" is the default name for the key so you can omit it entirely if your API does work this way.

Caveats

I ran into a couple of minor issues which I needed to solve for my purpose. These findings might also come in handy for you to know when you’ll start working with the RuntimeTypeAdapterFactory.

  1. If the parser encounters an unexpected type, an exception will be thrown and your complete object will be invalid — You can modify the parser to return null instead if you don’t like this approach
  2. The parsed "type" field will not be returned to the model — if you want this info, you’ll need to re-add it

Happy coding!

26