Unwrap Nested JSON with Different Structure on Retrofit

We want to parse nested JSON of the generic type because these responses have the same wrapper. This article will share our final solution and how to convert all ResponseBody to Data<T>, and then convert to T.

Bram Yeh
4 min readMar 14, 2019

Issue

In our current project (we use OKHttp3, Retrofit2, and Gson to connect our API server), there is a service that offers RESTful APIs with GraphQL-like responses. For example, when we use GET requests to https://[domain]/v1/cart?count=20, it responses

{
"data": { ... },
"extensions": { ... }
}

It is … a GraphQL’s response… looks exactly like it.

All we want are the nested JSON inside “data” which could be any formats. Without generic type converter, we will land ourselves in one of the following dilemmas:

A. Classes of wrapper and essence for each API response

public class Data<T> {
public T data;
}
public class Cart {
...
...
}
public interface RestfulService {
@GET(“/v1/cart”)
Call<Data<Cart>> getTotalCart();
}

Retrofit method responses Data<Cart> but not Cart directly.

B. Custom deserializer of each Class

@JsonAdapter(CartDeserializer.class)
public class Cart {
...
...
}
public class CartDeserializer implements JsonDeserializer<Cart> {
@Override
public Cart deserialize(JsonElement json,
Type typeOfT,
JsonDeserializationContext context) {
// Get the "data" essential from the parsed JSON
if (json.getAsJsonObject().has("data")) {
json = json.getAsJsonObject().get("data");
}
...
}

We need lots of custom deserializers with duplicated codes that parse “data.”

Objective

Let’s take an ultimate goal for all use cases, I want these both solutions’ advantages: every Retrofit method returns nested data directly without each custom deserializer. To give an example,

public class Cart { 
...
...
}
public class Promotions {
...
...
}
public interface RestfulService {
@GET(“/v1/cart”)
Call<Cart> getTotalCart();
@GET("/v1/promotions")
Call<Promotions> getPromotions();
}

Approach

First, take a customer Converter that parses the default ResponseBody (which comes from OKHttp3) to Data<T>, more precisely, to Data<Cart> or Data<Promotion>.

Second, after converting to Data<T>, we return the inner T data as our conclusive response.

Converter: from ResponseBody to generic T object

What we need first is a customized converter that extracts inner data; this DataConverter<T> returns the nested T data.

public class Data<T> {
public T data;
}
public class DataConverter<T> implements Converter<ResponseBody, T>
{
final private Converter<ResponseBody, Data<T>> mConverter;
public DataConverter(Converter<ResponseBody, Data<T>> converter)
{
mConverter = converter;
}
@Override
public T convert(ResponseBody value) throws IOException {
Data<T> dataModel = mConverter.convert(value);
return dataModel.data;
}
}
/**
* Kotlin (06/28/2019)
*/
class DataConverter<Any>(
private val delegate: Converter<ResponseBody, Data<Any>>?
) : Converter<ResponseBody, Any> {
override fun convert(value: ResponseBody): Any? {
val graphQLDataModel = delegate?.convert(value)
return graphQLDataModel?.data
}
}

DataConverter uses another converter to do the transformation (I will explain why in the later) from ResponseBody to Data<T>, and then it extracts T data as a result.

Converter.Factory: to generate customize Converter

And then we customize a Converter.Factory to return the DataConverter instance.

public class DataConverterFactory extends Converter.Factory {    @SuppressWarnings("unchecked")
@Nullable
public Converter<ResponseBody, ?> responseBodyConverter(
Type type,
Annotation[] annotations,
Retrofit retrofit)
{
try {
// Use TypeToken of Gson to get type literal for the parameterized type represented Data<T>
Type dataType = TypeToken.getParameterized(Data.class, type).getType();
Converter<ResponseBody, Data> converter = retrofit.nextResponseBodyConverter(this, dataType, annotations);
return new DataConverter(converter);
} catch (Exception e) {
return null;
}
}
}
/**
* Kotlin (06/28/2019)
*/
class DataConverterFactory : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *>? {
return try {
val dataType = TypeToken.getParameterized(Data::class.java, type).type
val converter: Converter<ResponseBody, Data<Any>>? = retrofit.nextResponseBodyConverter(this, dataType, annotations)
DataConverter(converter)
} catch (e: Exception) {
null
}
}
}

Gson TypeToken.getParameterized(Data.class, type) offers explicit type, e.g., Data<Cart> or Data<Promotion>.

Retrofit nextResponseBodyConverter(...) uses this explicit type to generate Converter<ResponseBody, Data> converter that transforms ResponseBody to Data<T>.

At last, DataConverter purifies the T data.

You might feel odd why I create a converter as a delegate for another converter DataConverter. I had tried to generate Converter<ResponseBody, Data> converter inside DataConverter directly. However, I need nextResponseBodyConverter() which need parameters of Converter.Factory, Type, and Annotation[]. After consideration, I decide to generate our Converter here and then inject it into DataConverter<T>.

Add Converter.Factory for deserialization of objects

new Retrofit.Builder()
.baseUrl([the base url])
.addConverterFactory(new DataConverterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(RestfulService.class);

Thanks Retrofit for Converter and Converter.Factory, we can extract all nested objects from RestfulService.

Footnote

Gson offers the TypeToken that represents a generic type T. And it is easy for us to use TypeToken.getParameterized(Data.class, type).getType() to get Data<T>’s type. If you use other libraries, here some suggestions you might need.

Gson

Type dataType = TypeToken.getParameterized(Data.class, type).getType();Converter<ResponseBody, Data> converter = retrofit.nextResponseBodyConverter(this, dataType, annotations);

Moshi

Type dataType = Types.newParameterizedType(Data.class, type);Converter<ResponseBody, Data> converter = retrofit.nextResponseBodyConverter(this, dataType, annotations);

Or … Others I don’t know

You might need to implement your ParameterizedType.

Type dataType = new ParameterizedTypeImpl(Data.class, new Type[]{type});Converter<ResponseBody, Data> converter = retrofit.nextResponseBodyConverter(this, dataType, annotations);////
public class ParameterizedTypeImpl implements ParameterizedType {
private final Class raw;
private final Type[] args;
public ParameterizedTypeImpl(Class raw, Type[] args) {
this.raw = raw;
this.args = args != null ? args : new Type[0];
}
@Override
public Type[] getActualTypeArguments() {
return args;
}
@Override
public Type getRawType() {
return raw;
}
@Override
public Type getOwnerType() {
return null;
}
}

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Bram Yeh
Bram Yeh

Written by Bram Yeh

Lead Android & iOS Mobile Engineer at Yahoo (Verizon Media) Taiwan https://www.linkedin.com/in/hanruyeh/

Responses (3)

Write a response