Consider yourself lucky if you have an API that gives you all the data you want in your app and nothing else. Unfortunately, that’s rarely ever the case.
Chances are, you deal with API responses that’s filled with 80% data you don’t need (or even understand why it’s there), 15% data you actually use in your app, and 5% missing data that’s supposed to be there but simply isn’t.
It probably won’t be in the nicest format either. The nodes in your API response might be nested to no end and you might want to traverse endlessly down the hierarchy just to build your simple 4-attribute data class.
[ { "weatherId": "wf8sj39s", "status": "confirmed", "data": [ "comment": "", "forecastedOn": "2018-12-03T22:06:15.761Z", "confirmedOn": null, "body": [ "current": { "weather": "partly_cloudy", "temperature": { "value": 12, "unit": "celsius" }, "extra_attributes": { "wind": 13, "humidity": 65, "precipitation": 0 } } ] ] } ]
Yeah. This isn’t the worst API response I’ve seen by any stretch. Far from it, but at first glance, this might take at least a few POJO data classes if I’d want to parse this automatically using GSON, Jackson, or Moshi.
What we don’t want to do is have to create a set of data classes like this.
data class WeatherModel( val weather: String, val temperature: Int, val wind: Int, val precipitation: Int, val humidity: Int ) data class WeatherDataModel( val weatherNode: WeatherNode ) data class WeatherNode( val data: WeatherData ) data class WeatherData( val weatherBody: WeatherBody )
And it only keeps on going…
But what if I told you there’s a way to take this response, pass it through a single function, and have it come out as a nicely laid out object with no nested structures and is what you’ll be passing into your activity?
Enter the deserializer.
What is a Deserializer
A deserializer is a class that allows you to parse a JSON response manually. You do this by consuming the api as a String, then build your object by traversing through the nodes in your JSON response by treating it like a map and picking out the pieces of data you require.
There are multiple ways to create a deserializer but for this tutorial, we’ll be making use of Jackson to make ours.
Set it up
To consume our api in the first place, we’ll be using Retrofit. If you’re not using Retrofit, well you should, and if there actually is a reason you’re not, I’ll be curious to hear it so stick it down in the comments.
implementation 'com.squareup.retrofit2:converter-scalars:2.1.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.7'
We will be adding an extra adapter to our Retrofit builder though, so add the ScalarConverterFactory dependency in addition to Jackson to your app/build.gradle
.
return new Retrofit.Builder() .baseUrl(WEATHERAPI_BASE_URL) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create(gson)) .build();
Then where you initialise your Retrofit, add the ScalarsConverterFactory in addition to any other factories you’re already using.
Implementing the Deserializer
Your deserializer must extend JsonDeserializer
provided to you by the Jackson dependency, with a type parameter of the model class you want coming out of it.
WeatherModelDeserializer.kt
class WeatherModelDeserializer: JsonDeserializer<WeatherModel>() { override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): WeatherModel { val oc = jp.codec val parentNode: JsonNode = oc.readTree(jp) val todaysWeatherNode = parentNode['data']['body']['current'] return WeatherModel( todaysWeatherNode['weather'].textValue(), todaysWeatherNode['temperature']['value'].asInt(), todaysWeatherNode['extra_attributes']['wind'].asInt(), todaysWeatherNode['extra_attributes']['precipitation'].asInt(), todaysWeatherNode['extra_attributes']['humidity'].asInt() ) } }
For the deserializer to work, you want to set up your Retrofit so you receive your api response as a string. This becomes possible because of the ScalarConverterFactory you added earlier.
@GET("api/someendpoint/weather") suspend fun weatherData(): String
Do the usual pattern of using an Api Service to execute your api call. You’re getting this as a string, so in your service (or repository, wherever you like really), map the string you got from the call to your model class like shown below.
fun createWeatherModel(weatherJson: String): WeatherModel { val mapper = ObjectMapper() val module = SimpleModule() module.addDeserializer(WeatherModel::class.java, WeatherModelDeserializer()) mapper.registerModule(module) val model = mapper.readValue<WeatherModel>(weatherJson, mapper.typeFactory.constructType(WeatherModel::class.java)) return model }
This function takes in the JSON in the form of a string, passes it through the deserializer, then returns the fully deserialized model class. Now you can efficiently deal with this data anywhere else in your app.
Benefits of Deserializers
Other than the more obvious use of parsing complex api responses, deserializers do have a number of advantages to them.
- Deserialization process becomes easily testable
- Errors in deserialization become easy to spot
- Changes in data structure of the api are easy to adapt to
All of these result in a parsing process that is more maintainable and scales better.
So I hope you found this tutorial useful. Learning how to use deserializers was definitely a turning point for me in defining how I choose to parse apis. I don’t use them all the time because rather often, using simple POJO data classes do suffice quite nicely. From time to time however, deserializers do come in very handy.
If you like this post, please give this a thumbs up, comment on it, share it on Twitter or LinkedIn, I don’t know, just give me something. I’m a small name and I have some big names to compete against in the world of Android blogging.
In any case, happy coding ༼ つ ◕_◕ ༽つ