How to handle dynamic JSON response with Retrofit

Note: This post is based on the widely used Retrofit2 networking library. Although the examples use a Gson converter, the same concept can be used with most of the other supported ones as well.

Imagine you’re in a situation where your backend can return a JSON response that’s dynamic in nature, a.k.a some parts of it don’t adhere to a specific pre-defined schema. Say you retrieve information about a webpage that you need to open in a WebView. You need to support both GET and POST HTTP requests, so two valid responses are:

{
	"url": "http://updated-website.com?product=123&size=big",
	"httpMethod": "GET"
}
{
	"url": "http://website.com",
	"httpMethod": "POST",
	"parameters" {
		"userId": "abc",
		"sessionId": "def",
		"randomParam1": "ghi",
		"randomParam2": "jkl"
	}
}

For the purpose of the example imagine that GSON can’t parse Maps automatically – the technique used here can be easily applied for more advanced examples as well. Now our problem is that we don’t know what and how many parameters there’ll be in the parameters section of the response, if present at all. Good news is that we don’t need to manipulate each individually, rather we can just put them in a Map that we’ll later pass to the WebView when making a POST request. So the Java model for our response can be:

class RedirectionInfo {
	private String uri;
	private String httpMethod;
	private Map<String, String> parameters;

    ... (constructors, getters, setters, etc.) ...
}

To read the response we can make use of Gson’s JsonDeserializer interface. It has a single deserialize() method where it’s our own responsibility to convert the incoming JSON to Java objects. Here’s a possible implementation of it:

class RedirectionInfoDeserializer implements JsonDeserializer<RedirectionInfo> {

    private static final String KEY_URI = "uri";
    private static final String KEY_METHOD = "httpMethod";
    private static final String KEY_PARAMETERS = "parameters";

    @Override
    public RedirectionInfo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        final JsonObject jsonObject = json.getAsJsonObject();

        // Read simple String values.
        final String uri = jsonObject.get(KEY_URI).getAsString();
        final String httpMethod = jsonObject.get(KEY_METHOD).getAsString();

        // Read the dynamic parameters object.
        final Map<String, String> parameters = readParametersMap(jsonObject);

        RedirectionInfo result = new RedirectionInfo();
        result.setUri(uri);
        result.setHttpMethod(httpMethod);
        result.setParameters(parameters);
        return result;
    }

    @Nullable
    private Map<String, String> readParametersMap(@NonNull final JsonObject jsonObject) {
        final JsonElement paramsElement = jsonObject.get(KEY_PARAMETERS);
        if (paramsElement == null) {
        	// value not present at all, just return null
            return null;
        }

        final JsonObject parametersObject = paramsElement.getAsJsonObject();
        final Map<String, String> parameters = new HashMap<>();
        for (Map.Entry<String, JsonElement> entry : parametersObject.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue().getAsString();
            parameters.put(key, value);
        }
        return parameters;
    }
}

In case you’re wondering what exactly is a JsonElement and how to parse it correctly, HERE’s a great tutorial on the subject, so definitely have a look!

Having this JsonDeserializer in place, all we need is to make use of it in Retrofit. Luckily the latter gives us the flexibility to use a custom converter to (de)serialize the request / response. The easiest way to do so is to pass a custom Gson to the GsonConverterFactory.create() method provided by Retrofit:

private Converter.Factory createGsonConverter() {
	GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(RedirectionInfo.class, new RedirectionInfoDeserializer());
    Gson gson = gsonBuilder.create();
    return GsonConverterFactory.create(gson);	
}

The third line is the most important one – it tells Gson to use our new custom deserializer every time it encounters a RedirectionInfo class. Of course we can register multiple different (de)serializers for different objects, so we can have all custom (de)serialization logic in one place.

Last step is just to use the custom factory we built:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("http://myserver.com")
    .addConverterFactory(createGsonConverter())
    .build();

CustomService service = retrofit.create(CustomService.class);
...

Submit a Comment

Your email address will not be published. Required fields are marked *