Dynamic Response Parser

A Cleaner Way to Retrofit

Rooparsh Kalia
6 min readJan 16, 2020

Hey, Folks, 👋🏻 I am back again with a brand new topic to discuss with you. This time we will be discussing a general problem that many developers face from time to time.

Before that, if you don’t know me or haven’t read my previous article please do give your valuable time to it too. You can visit the article here.

Nowadays, almost all the Android applications available in the market make an API request in one way or another. Rarely you will come across any static app. The process of calling any API request has been simplified a lot by Retrofit by Square.

Retrofit is a popular, simple and flexible library to handle network requests in the android system. In their own words, Retrofit is

“A type - safe HTTP client for Android and Java”

The ability for it to be adapted for different use cases is quite amazing as you can inject custom interceptors to intercept, change or modify request/response; call adapter factory for supporting service method return types other than usual Call as well as converter factory for serialisation & deserialisation of object using Gson, Moshi, Jackson etc.

Coming back to the topic; we (android developers) have to make API requests to the server for showing data in our app. This data can be in the form of JSON, XML or YAML, etc. I believe you guys know a lot about Retrofit configuration, Converter Factory, and stuff, and in case you don’t, you can always read their official doc.

Most of the time throughout the application the data response is uniform. Either the APIs are returning the JSON object or XML object. But sometimes, a situation may arise where we have to call APIs which are returning different object formats.

What to do in that case?? 🤔 hmmm….

Well not to worry, Retrofit provides us a way of adding multiple converter factories. There are various factory-built converters that you can use. GsonConverterFactory, MoshiConverterFactory, etc to name some. We can chain as many converters as we want. But, there’s an issue with chaining converters. If not handled properly, the object can leak from one converter to another.

Pic Credit: Andrey Popov

Also sometimes, the response from an API doesn’t follow any standard structure. In that case, our standard converters are bound to fail.

[
"test" : 123
]

A sample of invalid json and the error

Parse error on line 2:
[ "test" : 123]
------------^
Expecting 'EOF', '}', ',', ']', got ':'

We only have 2 options in this case. Either we ask the backend guy to correct the response, or we create a custom converter of your own. (Remember Retrofit is a flexible library… so it supports custom converters also 😉).

source : Wordpress

So, I faced an API that was sending a JSONP response while all other APIs were sending normal JSON (well if you don’t know what jsonp is …you can read more about it here 😬..you can thank me later). So coming back to the story, our normal json parser failed in this scenario, but still we need our regular json parser here, along with our own customized parser. Because the APIs were not developed in house, as it was a third party, so asking them to correct the response was out of the question. So I took the second road.

The response in question is :

Sample of JsonP response.

And the crash logs are:

MalperformedJsonException

So let’s get started with creating our own Parser for handling the JSONP response, the one shown above.

TL;DR

Steps :

Step1: Create Custom logic converter

Step2: Create Converter Factory

Step3: Create Annotations

Step4: Create Super converter class and add it to Retrofit builder

Step 1: Create a Converter, where you will implement your custom logic. This parser will handle the parenthesis ( “(”, “)” ) along with the callback function. Thus giving us our regular json expression.

import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import okhttp3.ResponseBody
import retrofit2.Converter
import java.io.IOException
import java.io.Reader
class GsonPResponseBodyConverter<T>(
private val gson: Gson,
private val adapter: TypeAdapter<T>
) :
Converter<ResponseBody, T> {

@Throws(IOException::class) override fun convert(value: ResponseBody): T {
val reader: Reader = value.charStream()
var item: Int = reader.read()
while (item != '('.toInt() && item != -1) {
item = reader.read()
}
val jsonReader: JsonReader = gson.newJsonReader(reader)

reader.use {
return adapter.read(jsonReader)
}

}

}

Step 2: Create a Converter Factory class for handling our custom converter.

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type

class GsonPConverterFactory
private constructor(private val gson: Gson) : Converter.Factory() {

override fun responseBodyConverter(
type: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *>? {
val adapter = gson.getAdapter(TypeToken.get(type))
return GsonPResponseBodyConverter(gson, adapter)

}

companion object {
fun create() = GsonPConverterFactory(Gson())

fun create(gson: Gson?): GsonPConverterFactory {
if (gson == null) {
throw NullPointerException("gson==null")
}
return GsonPConverterFactory(gson)
}
}
}

Step 3: Now here lies a question which you must ask yourself. Does your app supports only one data format for the response throughout the app or does it need different kinds of response objects?

If your answer is that your application supports only one kind of data format for a response, then just add your custom converter just like any normal converter you add to your retrofit object.

return Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonPConverterFactory.create())
.client(okHttpClient)
.build()

But if you want multiple response formats at the same time then jump to the next step.

Step 4: Create annotations for different response formats.

annotation class JsonP

annotation class Json

annotation class XML

annotation class PlainText

Step 5: Add these annotations while creating the API service.

@FormUrlEncoded
@POST("example/add_student")
@JsonP // <----------------------- IMPORTANT PART
fun addStudent(
@Field("name") name: String,
@Field("age") age: Int,
@Field("address") address: String): Response<TokenData>

@POST("example/authorize")
@Json // <-------------------------------- Don't Forget
fun authorize(
@Body dummyData: DummyData): Response<ResponseObject>
@GET("example/list_students")
@XML // <-------------------------------- Don't Forget
fun getListOfStudents(): Response<Student>

Here, for the first API, we want our custom converter which we just created, and for the second and the third API, our regular GsonConverterfactory and SimpleXmlConverterFactory will do the case.

Step 6: Create a Super Converter for handling all response objects.

class DynamicConverterFactory private constructor(gson: Gson) : Converter.Factory() {

private val jsonP: Converter.Factory by lazy {
GsonPConverterFactory.create()
}

private val json: Converter.Factory by lazy {
GsonConverterFactory.create(gson)
}

private val xml: Converter.Factory by lazy {
SimpleXmlConverterFactory.create(gson)
}

override fun responseBodyConverter(
type: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *>? {
annotations.forEach { annotation ->

return when (annotation.annotationClass) {
Json::class -> json.responseBodyConverter(type, annotations, retrofit)
JsonP::class -> jsonP.responseBodyConverter(type, annotations, retrofit)
Xml::class -> xml.responseBodyConverter(type, annotations, retrofit)
else -> null
}
}
return null
}

companion object {
fun create() = DynamicConverterFactory(Gson())
}
}

Step 6: Add this Converter to your retrofit object.

return Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(DynamicConverterFactory.create())
.client(okHttpClient)
.build()

and voila… you are good to go …

Now your converter can handle easily JSON, JSONP and XML all through the single converter .🎉🎉🎉.

We can improvise our converter a lot here, but let’s leave that to some another story for some another day.

Leave a comment for any doubt or for any suggestion regarding any topic you would love to discuss. Follow me on twitter for more updates.

Keep supporting guys, will be back soon with another topic to discuss.

Keep Learning. Keep Growing.

--

--

Rooparsh Kalia

Software Engineer trying to swim in the ocean of technology