Understanding MVVM on Android Tutorial 02 – Defining the Model with GSON and POJOs

Over the course of this series, we’ll be building an app that uses RESTful APIs to fetch data in JSON format and display it in our app. A good place to start (…in any development project) is to define the model layer. What is the business domain for the app? What high level objects can be used to represent the business domain? And what are the relationships between the various business objects? As an example, if we were defining the model for a school administration system, a first attempt at defining the model may produce the following objects: Student, Course, and Staff. In our case, we will be building a “GitHub Repo Reader” app that fetches and displays all the repositories of a single github user.

*We are using the GitHub API because the endpoints do not require authentication and our selected GitHub user is Square. Github also has a very well-documented API available here.

To return Square’s repositories, we will call the endpoint: https://api.github.com/users/square/repos.
Navigating to this url, we see that the JSON returns a list of repositories in the format:

[
  {
    "id": 36665193,
    "name": "Aardvark",
    "full_name": "square/Aardvark",
    "owner": {
      "login": "square",
      "id": 82592,
      "avatar_url": "https://avatars.githubusercontent.com/u/82592?v=3",
      "gravatar_id": "",
      "url": "https://api.github.com/users/square",
      "html_url": "https://github.com/square",
      "followers_url": "https://api.github.com/users/square/followers",
      "following_url": "https://api.github.com/users/square/following{/other_user}",
      "gists_url": "https://api.github.com/users/square/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/square/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/square/subscriptions",
      "organizations_url": "https://api.github.com/users/square/orgs",
      "repos_url": "https://api.github.com/users/square/repos",
      "events_url": "https://api.github.com/users/square/events{/privacy}",
      "received_events_url": "https://api.github.com/users/square/received_events",
      "type": "Organization",
      "site_admin": false
    },
    "private": false,
    "html_url": "https://github.com/square/Aardvark",
    "labels_url": "https://api.github.com/repos/square/Aardvark/labels{/name}",
    "releases_url": "https://api.github.com/repos/square/Aardvark/releases{/id}",
    "deployments_url": "https://api.github.com/repos/square/Aardvark/deployments",
    "created_at": "2015-06-01T14:14:36Z",
    "updated_at": "2017-02-22T03:22:46Z",
    "pushed_at": "2017-01-12T16:55:23Z",
    "git_url": "git://github.com/square/Aardvark.git",
    "ssh_url": "git@github.com:square/Aardvark.git",
    "clone_url": "https://github.com/square/Aardvark.git",
    "svn_url": "https://github.com/square/Aardvark",
    "homepage": null,
    "size": 10081,
    "stargazers_count": 165,
    "watchers_count": 165,
    "language": "Objective-C",
    "has_issues": true,
    "has_downloads": true,
    "has_wiki": true,
    "has_pages": false,
    "forks_count": 18,
    "mirror_url": null,
    "open_issues_count": 1,
    "forks": 18,
    "open_issues": 1,
    "watchers": 165,
    "default_branch": "master"
  },
  {
    "id": 60724575,
    "name": "Ackbar",
    "full_name": "square/Ackbar",
    "owner": {
      "login": "square",
      "id": 82592,
      "avatar_url": "https://avatars.githubusercontent.com/u/82592?v=3",
      "gravatar_id": "",
      "url": "https://api.github.com/users/square",
      "html_url": "https://github.com/square",
      "followers_url": "https://api.github.com/users/square/followers",
      "following_url": "https://api.github.com/users/square/following{/other_user}",
      "gists_url": "https://api.github.com/users/square/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/square/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/square/subscriptions",
      "organizations_url": "https://api.github.com/users/square/orgs",
      "repos_url": "https://api.github.com/users/square/repos",
      "events_url": "https://api.github.com/users/square/events{/privacy}",
      "received_events_url": "https://api.github.com/users/square/received_events",
      "type": "Organization",
      "site_admin": false
    },
    "private": false,
    "html_url": "https://github.com/square/Ackbar",
    "labels_url": "https://api.github.com/repos/square/Ackbar/labels{/name}",
    "releases_url": "https://api.github.com/repos/square/Ackbar/releases{/id}",
    "deployments_url": "https://api.github.com/repos/square/Ackbar/deployments",
    "created_at": "2016-06-08T19:25:00Z",
    "updated_at": "2017-01-25T04:28:16Z",
    "pushed_at": "2016-06-14T16:34:31Z",
    "git_url": "git://github.com/square/Ackbar.git",
    "ssh_url": "git@github.com:square/Ackbar.git",
    "clone_url": "https://github.com/square/Ackbar.git",
    "svn_url": "https://github.com/square/Ackbar",
    "homepage": null,
    "size": 20,
    "stargazers_count": 18,
    "watchers_count": 18,
    "language": "Swift",
    "has_issues": true,
    "has_downloads": true,
    "has_wiki": true,
    "has_pages": false,
    "forks_count": 1,
    "mirror_url": null,
    "open_issues_count": 0,
    "forks": 1,
    "open_issues": 0,
    "watchers": 18,
    "default_branch": "master"
  }
]

From our returned JSON, we can say that the business domain for our “GitHub Repo Reader” application contains the following objects:

 

Digging into the GitHub API, it is revealed that a Repo can only have one Owner, so the relationship between the business objects can be modeled as shown below.

 

Now that we have identified our objects and defined the relationships between them, we can go on to create a POJO (Plain Old Java Object) for each business object in our application domain.

The POJO for our Repo object can be represented as thus:

public class Repo {
    private int id;
    private String name;
    private String url;
    private Owner owner;
    
}

Note that the Repo object contains a nested Owner object, which is defined in its own POJO.

The POJO for our Owner object can be represented as:

public class Owner {
    private int id;
    private String login;
    private String url;
    private String avatar_url;

}

Note that we have not captured all the fields for the Repo or Owner object from the JSON. We have mapped the fields we are interested in and the undefined fields with be ignored by GSON.

At this point, it is probably worth explaining how GSON works with names. Taking our Owner object as an example, we can see the field names in our POJO are exactly the same as they are returned in the JSON. But what if we wanted to use our own custom names? For example, instead of “avatar_url” we want to call our image link “imageUrl”. Then we would use a SerializedName annotation for that field as shown below.

public class Owner {
    private int id;
    private String login;
    private String url;

    @SerializedName("avatar_url")
    private String imageUrl;

}

Luckily, in our “GitHub Reader” application, the JSON response is pretty straightforward and as shown above, we can parse it with the creation of two simple classes: Repo and Owner. But you may come across scenarios where your JSON response is a bit more complicated and you will need to build a custom deserializer in order to parse your JSON response correctly.

For example, let’s have a look at the JSON response from the Reddit REST API endpoint: http://reddit.com/top.json

 

We can see that JSON string returned is recursive, i.e. the main object has a kind object and data object which in turn contains children objects which have a kind object and data object. We “could” create the following POJOs to parse the string:

A RedditObject Class:

public class RedditObject {
    public RedditData data;

    public RedditData getData() {
        return data;
    }

A RedditData Class:

public class RedditData {
    public List<RedditChildren> children;

    public List getRedditChildren() {
        return children;
    }

A RedditChildren Class:

public class RedditChildren {
    String kind;
    RedditLink data;
}

A RedditLink Class:

public class RedditLink {
    private String id;
    private String title;
    private String domain;
    private String subreddit;
    private String subredditId;
    private String linkFlairText;
    private String author;
    private String thumbnail;
    private String permalink;
    private String url;
    private int score;
    private int ups;
    private int downs;
    private int numComments;
    private boolean over18;
    private boolean hideScore;
    /* Date fields below need further manipulation
     * so cannot be parsed with our plain POJO, 
     * we need a custom deserializer to properly handle these fields */
    //private Date created;
    //private Date createdUtc;

}

We could use the four classes above to parse the response from Reddit but a better approach is to create a custom deserializer to parse the JSON response recursively. Using a custom deserializer will also allow us to include deserializers for any fields that need further manipulation e.g. the Date fields.

The RedditObjectJsonDeserializer Class

/**
 * Deserialize the Reddit object into a subclass based on its 'kind' field.
 */
public class RedditObjectJsonDeserializer implements JsonDeserializer
{
    @Override
    public RedditObject deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
    {
        if (json == null || !json.isJsonObject())
        {
            return null;
        }

        try
        {
            JsonObject jsonObject = json.getAsJsonObject();
            String kind = jsonObject.get("kind").getAsString();

            return context.deserialize(jsonObject.get("data"), getClassForKind(kind));
        }
        catch (JsonParseException e)
        {
            return null;
        }
    }

    private Class getClassForKind(String kind)
    {
        switch (kind)
        {
            case "Listing":
                return RedditListing.class;
            case "t3":
                return RedditLink.class;
            default:
                return null;
        }
    }
}

}

The RedditDateDeserializer Class

/**
 * Reddit uses timestamps for dates. This deserializer transforms them into Java dates;
 */
public class RedditDateDeserializer implements JsonDeserializer
{
    @Override
    public Object deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
    {
        return new Date(json.getAsJsonPrimitive().getAsLong() * 1000);
    }
}

Lets have a look at another example. In some situations your JSON response may be “dynamic”. That is, the returned object names are not static but are randomly generated like the example shown below.

{
	"abd970679A706398729":
	{
		"owner":
		{
			"name": "Kyubid",
			"url": "https://kyubid.com"		
		},
		"comments": 15,
		"created_time": "2017-02-17T16:50:00+00:00",
		"id": "52327487706398729",
		"type": "post"
	},
	"def972999B386166761":
	{
		"owner":
		{
			"name": "Kyubid",
			"url": "https://kyubid.com"	
		},
		"comments": 30,
		"created_time": "2017-02-18T13:12:53+00:00",
		"id": "52327486166761",
		"type": "post"
	},
	"ghi97354C3366112363":
	{
		"Owner":
		{
			"name": "Kyubid",
			"url": "https://kyubid.com"	
		},
		"comments": 4,
		"created_time": "2017-02-19T09:16:33+00:00",
		"id": "5232748366112363",
		"type": "post"
	}
}

This is another situation where you will definitely need to implement a custom deserializer like the class shown below to correctly parse the JSON response.

public class PostObjectJsonDeserializer implements JsonDeserializer {
    Gson gson = new Gson();

    @Override
    public PostListing deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
    {
        if (element == null || !element.isJsonObject())
        {
            return null;
        }

        JsonObject json = element.getAsJsonObject();

        PostListing postListing = new PostListing();

        for (Map.Entry<String, JsonElement> entry : json.entrySet())
        {
            Post post = gson.fromJson(entry.getValue(), Post.class);

            postListing.getPosts().add(post);
      }

        return postListing;
    }
}

The PostListing Class

public class PostListing {

    private List posts = new ArrayList<>();

    public List getPosts() {
        return posts;
    }
}

The Post Class

public class Post {
    private String created_time;
    private String id;
    private String type;
    private int comments;
    private Owner owner;
}

The Owner Class

public class Owner {
    private String name;
    private String url;
}

Further Reading and Comments: You may find it helpful to eyeball the structure of your JSON response when you are creating your POJOs. The following resources can help:
http://www.freeformatter.com/json-formatter.html
https://jsonformatter.curiousconcept.com/
And, you can even have your POJOs created for you using this tool: http://www.jsonschema2pojo.org/

Key Takeaway: An area you are likely to get caught out while modeling your objects with GSON is understanding how GSON handles objects {….} vs arrays [….] . Confuse the two and you will end up with the dreaded error message “java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT” or vice versa. But we will do a deep-dive into this in our next series, where we discuss retrieving and parsing the objects with the use of the Retrofit2 library.

 

Upcoming posts in the series:
Understanding MVVM on Android Tutorial 03 – Networking with Retrofit2
Understanding MVVM on Android Tutorial 04 – Creating your View with RecyclerViews

Leave a Reply