Gson is great and really allows applying custom deserialization strategies, or implementing adapters to JSON structures that are not very user-friendly. Suppose you are fine to have the following custom mapping to search query results:
final class Suggestions {
final String query;
final List<String> suggestions;
Suggestions(final String query, final List<String> suggestions) {
this.query = query;
this.suggestions = suggestions;
}
}
Writing a custom JsonDeserializer
(consider it's just a JSON to a Java object/value tranformer) where you define how to parse the given JSON payload and convert it to a Suggestions
class instance:
final class SuggestionsJsonDeserializer
implements JsonDeserializer<Suggestions> {
private static final JsonDeserializer<Suggestions> suggestionsJsonDeserializer = new SuggestionsJsonDeserializer();
private static final Type listOfStringsType = new TypeToken<List<String>>() {
}.getType();
private SuggestionsJsonDeserializer() {
}
static JsonDeserializer<Suggestions> getSuggestionsJsonDeserializer() {
return suggestionsJsonDeserializer;
}
@Override
public Suggestions deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
throws JsonParseException {
if ( !jsonElement.isJsonArray() ) {
throw new JsonParseException("The given JSON is not an array");
}
final JsonArray jsonArray = jsonElement.getAsJsonArray();
final int length = jsonArray.size();
if ( length != 2 ) {
throw new JsonParseException("The given JSON array length is " + length);
}
final JsonElement e0 = jsonArray.get(0);
final JsonElement e1 = jsonArray.get(1);
final String query;
final List<String> suggestions;
if ( e0.isJsonPrimitive() && e1.isJsonArray() ) {
// If the JSON array is [query, suggestions]
query = e0.getAsJsonPrimitive().getAsString();
suggestions = context.deserialize(e1.getAsJsonArray(), listOfStringsType);
// e1.getAsJsonArray() call is unnecessary because the context would throw an exception if it would be not an array
// But just make it explicit and "more symmetric" to the object destructuring around
// Another way might be not delegating the string list parsing to context, but building a string list out of the JSON array of strings manually
} else if ( e0.isJsonArray() && e1.isJsonPrimitive() ) {
// If the JSON array elements are swapped like [suggestions, query]
query = e1.getAsJsonPrimitive().getAsString();
suggestions = context.deserialize(e0.getAsJsonArray(), listOfStringsType);
} else {
throw new JsonParseException("Unexpected JSON array structure");
}
return new Suggestions(query, suggestions);
}
}
The next step is making Gson to be aware of your mapping and its JSON deserializer pair:
final Gson gson = new GsonBuilder()
.registerTypeAdapter(Suggestions.class, getSuggestionsJsonDeserializer())
.create();
Testing it without Retrofit
final Suggestions suggestions = gson.fromJson(JSON, Suggestions.class);
out.println(suggestions.query);
out.println(suggestions.suggestions);
will output:
football
[football, football skills, football vines, football fails, football manager 2017, football challenge, football respect, football manager 2017 download, football factory, football daily]
In order to make Retrofit aware of your custom Gson
instance, you just need to register it in Retrofit.Builder
with .addConverterFactory(GsonConverterFactory.create(gson))
for Retrofit 2, or .setConverter(new GsonConverter(gson))
for Retrofit 1 (if I'm not wrong).