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.
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
insideDataConverter
directly. However, I neednextResponseBodyConverter()
which need parameters of Converter.Factory, Type, and Annotation[]. After consideration, I decide to generate our Converter here and then inject it intoDataConverter<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;
}
}