Tuesday, February 2, 2016

Deserialize JSON data returned from Retrofit, using GSON.

This tutorial takes off right where we ended the previous Retrofit tutorial.
At this point we have downloaded data and we have a single WeatherData object coming back from the service.

What we want now is to get the additional data from the JSON.
Before we continue, let's look at the sample JSON again.

Sample JSON

{
 "cnt": 3,
 "list": [{
  "coord": {
   "lon": 37.62,
   "lat": 55.75
  },
  "sys": {
   "message": 0.0045,
   "country": "RU",
   "sunrise": 1454390479,
   "sunset": 1454421906
  },
  "weather": [{
   "id": 803,
   "main": "Clouds",
   "description": "broken clouds",
   "icon": "04n"
  }],
  "main": {
   "temp": -1.4,
   "temp_min": -1.401,
   "temp_max": -1.401,
   "pressure": 1001.24,
   "sea_level": 1021.82,
   "grnd_level": 1001.24,
   "humidity": 93
  },
  "wind": {
   "speed": 3.55,
   "deg": 219.507
  },
  "clouds": {
   "all": 80
  },
  "dt": 1454380676,
  "id": 524901,
  "name": "Moscow"
 }, {
  "coord": {
   "lon": 30.52,
   "lat": 50.43
  },
  "sys": {
   "type": 1,
   "id": 7358,
   "message": 0.0282,
   "country": "UA",
   "sunrise": 1454391071,
   "sunset": 1454424722
  },
  "weather": [{
   "id": 800,
   "main": "Clear",
   "description": "Sky is Clear",
   "icon": "01n"
  }],
  "main": {
   "temp": -0.66,
   "pressure": 1016,
   "humidity": 100,
   "temp_min": -1,
   "temp_max": 0
  },
  "visibility": 10000,
  "wind": {
   "speed": 4,
   "deg": 220
  },
  "clouds": {
   "all": 0
  },
  "dt": 1454378400,
  "id": 703448,
  "name": "Kiev"
 }, {
  "coord": {
   "lon": -0.13,
   "lat": 51.51
  },
  "sys": {
   "message": 0.0141,
   "country": "GB",
   "sunrise": 1454398618,
   "sunset": 1454431885
  },
  "weather": [{
   "id": 804,
   "main": "Clouds",
   "description": "overcast clouds",
   "icon": "04n"
  }],
  "main": {
   "temp": 9.22,
   "temp_min": 9.224,
   "temp_max": 9.224,
   "pressure": 1017.04,
   "sea_level": 1027.01,
   "grnd_level": 1017.04,
   "humidity": 74
  },
  "wind": {
   "speed": 10.5,
   "deg": 255.507
  },
  "clouds": {
   "all": 92
  },
  "dt": 1454380483,
  "id": 2643743,
  "name": "London"
 }]
}
Remember, I'm getting the sample JSON from Open Weather Map

 

Understand JSON

Note: if you already understand what JSONs are, you may want to skip this section.
JSON strings are super easy. They are based on two concepts: JSON Objects and JSON Arrays, and key value pairs.

Key value pairs

These are 2 words that are separated with a colon (:), and each key value pair is separated with a comma (,). They are presented as this:

"key1":"value1", "key2":"value2","myKey":"my value","monster":"Cookie Monster", "myAge":32

Keys can have spaces, but they usually have issues on some programming languages, so that makes most keys to always be without spaces.
Values can be whatever you want. If they're Strings then they have quotes ("") around them, while integers and doubles don't have them. However, sometimes the output from the log from an IDE might place quotes around everything.

JSON Objects and JSON Arrays

By default a single JSON string contains a single JSON Object, but the most important part to remember about JSON Objects and JSON Arrays is this:

JSON Object are represented as {} (curly braces)
JSON Arrays are (usually) represented as [] (square brackets)

Note: XCode usually outputs JSON Arrays as () (parenthesis)

So with that, we can create a JSON Object like this:
{
 "key1": "value1",
 "monster": "Cookie Monster"
}
This is a JSON Object with 2 key values (we call that fields now)
Before we talk about JSON Arrays, you need to see what we can do now: we can do a key value pair with a key a JSON Object, like this:
{
 "key1": "value1",
 "monster": "Cookie Monster",
 "carObject": {
  "brand": "Ford",
  "model": "Model A"
 },
 "computerObject": {
  "brand": "Apple",
  "model": "Macbook pro"
 }
}
In here, we not have a key of carObject with a value of a new JSON Object. This new JSON Object has it's own set of key values. Same thing with computerObject.

JSON Arrays are basically what you would expect them to be: an array of JSON Objects. The only thing to remember is that JSON Arrays wrap JSON Objects, so they are represented before a curly brace.
Here's an example of a JSON Object with a JSON Array:
{
 "key1": "value1",
 "monster": "Cookie Monster",
 "carObject": [{
  "brand": "Ford",
  "model": "Model A"
 }, {
  "brand": "Chevy",
  "model": "Camaro"
 }]
}
So now inside the key of carObject we now have a JSON Array of JSON Objects. In this case we have 2 JSON Objects, each of them with their own key value pairs.

And with that under our belt, we go back to our Android application to parse our JSON.

Create Java objects

The point of our deserialization is to end up with POJOs (Plain Old Java Objects) out of this whole deal. In order to do that, we need to deserialize our JSON string with POJOs in the same order of our JSON. Let's look back at the JSON, in the simplest form (just 1 group instead of 3):
{
 "cnt": 3,
 "list": [{
  "coord": {
   "lon": 37.62,
   "lat": 55.75
  },
  "sys": {
   "message": 0.0045,
   "country": "RU",
   "sunrise": 1454390479,
   "sunset": 1454421906
  },
  "weather": [{
   "id": 803,
   "main": "Clouds",
   "description": "broken clouds",
   "icon": "04n"
  }],
  "main": {
   "temp": -1.4,
   "temp_min": -1.401,
   "temp_max": -1.401,
   "pressure": 1001.24,
   "sea_level": 1021.82,
   "grnd_level": 1001.24,
   "humidity": 93
  },
  "wind": {
   "speed": 3.55,
   "deg": 219.507
  },
  "clouds": {
   "all": 80
  },
  "dt": 1454380676,
  "id": 524901,
  "name": "Moscow"
 }]
}
In the Java side, we already have a class named WeatherData, and inside here we have this:
package eduardoflores.com.test_networkconnection;

import com.google.gson.annotations.SerializedName;

/**
 * @author Eduardo Flores
 */
public class WeatherData {

    @SerializedName("cnt")
    public int count;
}
We know this works, but now we need to move to the JSON key of list. When you look at the list key in the JSON you can kind of see that while the key itself says list, this is more like a location. So we will create a new Location.java class.
With the new Location class, we will update our WeatherData to get the list key as a Location object, and we will call this variable location.
package eduardoflores.com.test_networkconnection;

import com.google.gson.annotations.SerializedName;

import java.util.List;

/**
 * @author Eduardo Flores
 */
public class WeatherData {

    @SerializedName("cnt")
    public int count;

    @SerializedName("list")
    public List<Location> location;
}
Notice how the location variable is actually a List of Location object. Why? Because the JSON string returns a JSON Array for the key of list.

With that, we move to the Location object.
The Location object contains multiple other objects: coords, sys and weather.
We will create a new java class named Coordinates for the coords key, and a Weather class for the weather key. I decided to skip the sys object to demonstrate that not every key needs to be deserialized.

With the new Coordinates and Weather classes created, the Location object now looks like this:
package eduardoflores.com.test_networkconnection;

import com.google.gson.annotations.SerializedName;

import java.util.List;

/**
 * @author Eduardo Flores
 */
public class Location {

    @SerializedName("coord")
    public Coordinates coordinates;

    @SerializedName("weather")
    public List<Weather> weather;

}
This should be simple now for you, but we will continue into the Weather class to show one last thing.
In the Weather java class, add fields for id, main, description and icon:
package eduardoflores.com.test_networkconnection;

import com.google.gson.annotations.SerializedName;

/**
 * @author Eduardo Flores
 */
public class Weather {

    @SerializedName("id")
    public Integer weatherId;

    public String main;

    public String description;

    public String icon;

}
This should show one last thing: while you can use the @SerializedName annotation for all your fields, you don't really need to add it if you want to create a variable using the same name of the field. For example, the JSON Object returns the key of description, and that is fine with me so I created the variable as description as well, and since they're both the same I don't have to use the @SerializedName annotation.

Testing it!

So before we run the app, let's go back to the MainClass.java (created in the previous tutorial) and into the success method of the callback.
In here, we just want to verify things work, so let's create some log statements, like this:
public Callback<WeatherData> weatherCallback = new Callback<WeatherData>() {
    @Override
    public void success(WeatherData weatherQuery, Response response) {
        Log.i("MY_APP", "count = " + weatherQuery.count);
        Log.i("MY_APP", "latitude = " + weatherQuery.location.get(0).coordinates.latitude);
        Log.i("MY_APP", "weather description = " + weatherQuery.location.get(0).weather.get(0).description);
    }

    @Override
    public void failure(RetrofitError error) {
        Log.e("MY_APP", error.getLocalizedMessage());
    }
};
(Yes, create the coordinates object by yourself. It's easy!)

With that, the output should be something like this:
02-01 20:00:09.930 10210-10210/eduardoflores.com.test_networkconnection I/MY_APP: count = 3
02-01 20:00:09.930 10210-10210/eduardoflores.com.test_networkconnection I/MY_APP: latitude = 55.75
02-01 20:00:09.930 10210-10210/eduardoflores.com.test_networkconnection I/MY_APP: weather description = broken clouds

And there you have it! You should now be able to deserialize any JSON string that gets thrown at you using GSON and Retrofit.

No comments:

Post a Comment