Unwrapping data with Retrofit 2

Retrofit is my library of choice when communicating with HTTP services in Java. One of my favourite features in Retrofit 2 is it's Converter (and more specifically it's new counterpart — Converter.Factory) API.

Envelopes

Let's take a common use case — when APIs wrap the data they return in an envelope type. For instance, Foursquare's API returns the following JSON structure:

{
  "meta": ...,
  "notifications": ...,
  "response": ...,
}

This JSON can be represented simply as a Java type.

class Envelope<T> {
  Meta meta;
  Notifications notifications;
  T response;
}

And naively, our API declaration could use this envelope directly.

interface FoursquareAPI {
  @GET("/venues/explore")
  Call<Envelope<Venues>> explore();
}

But wouldn't it be better if you could ignore this envelope, and work with your desired types directly? Your client wouldn't have to know about the Envelope type at all, and not have to worry about unrwapping it manually.

interface FoursquareAPI {
  @GET("/venues/explore")
  Call<Venues> explore();
}

Converter

In Retrofit, a Converter is a mechanism to convert data from one type to another. Retrofit doesn't ship with any converters by default, but provides modules backed by popular serialization libraries. We'll lean on these modules for the heavy lifting, but write some custom code to get our desired behaviour.

First, we write a converter that first parses the data as an Envelope<T> object by delegating the work to another converter. Once the data is parsed, our converter extracts our desired response from the Envelope object.

class EnvelopeConverter<T> implements Converter<ResponseBody, T> {
  final Converter<ResponseBody, Envelope<T>> delegate;

  EnvelopeConverter(Converter<ResponseBody, Envelope<T>> delegate) {
    this.delegate = delegate;
  }

  @Override
  public T convert(ResponseBody responseBody) throws IOException {
    Envelope<T> envelope = delegate.convert(responseBody);
    return envelope.response;
  }
}

Converter Factory

When we create our Retrofit instance, we can give it Converter.Factory instances. Retrofit will look up these factories (in order) and ask them to return a converter if they can deserialize to a given Java type.

We'll need to create a custom factory that returns our EnvelopeConverter to let Retrofit know about our custom converter. Our custom factory will also ask Retrofit to give us the "next" converter that would have deserialized the Envelope<T> type if our custom converter didn't exist. This is the converter that the EnvelopeConverter delegates to.

class EnvelopeConverterFactory extends Converter.Factory {
  @Override
  Converter<ResponseBody, ?> responseBodyConverter(
      Type type,
      Annotation[] annotations,
      Retrofit retrofit) {
    Type envelopeType = Types.newParameterizedType(Envelope.class, type);
    Converter<ResponseBody, Envelope> delegate =
        retrofit.nextResponseBodyConverter(this, envelopeType, annotations);
    return new EnvelopeConverter(delegate);
  }
}

Putting it all together

Armed with our factory, we can create our Retrofit instance. We still need to supply our "next" converter (Moshi in this example) that our EnvelopeConverter will use to deserialize the Envelope<T> type.

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("http://localhost:1234")
        .addConverterFactory(new EnvelopeConverterFactory())
        .addConverterFactory(MoshiConverterFactory.create())
        .build();

After wiring this all up, we can use our simplified API. And it'll pay dividends as the complexity of our apps and API grow

interface Service {
  @GET("/venues/explore")
  Call<Venues> explore();
}

If you'd like to see a complete working example, this is the approach we've used in our JSON-RPC client powered by Retrofit.

Beyond Converters

Converters barely scratch the tip of the surface when it comes to customizing Retrofit's functionality. If you'd like to hack around even more, I'd recommend checking out Retrofit's CallAdapter API, which powers functionality such as it's RxJava integration, and other use cases you could previously only dream of.