Building Data Models for APIs and Android

I’m moving my attention a little bit towards native development now, mostly investigating Kotlin and Java for Android development. Swift for iOS development is also on my radar. One of the things I regularly do is create small applications to learn a technology. My current interest is producing a weather app. Now, that sounds boring. There are, after all, a huge number of perfectly acceptable weather apps. I’m not going to publish it – just write it. To produce my weather app, I’m going to use an API provided by OpenWeatherMap. Their SDK documentation is reasonable, and they provide many ways of getting samples of responses so you can convert it.

When requesting the API, the response comes back in JSON format, and I need to convert that to something that my application can actually read. Google provides a nice Android library for serializing and deserializing JSON called “gson“. Before using GSON serialization and deserialization, you must add the GSON library to your app build.gradle file:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.3"
    defaultConfig {
        applicationId "com.shellmonger.weatherapp"
        minSdkVersion 19
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.google.code.gson:gson:2.8.0'
    testCompile 'junit:junit:4.12'
}

Most of this is from the standard new project version. The highlighted line is what I added to bring in the GSON library.

Serializing a model involves calling the toJson() method. I generally override the toString() method or add a toJson() method to my model for generating the JSON. Similarly, there is a fromJson() method that takes a JSON string and returns an object. I generally add a static fromJson() method to my model to product the new object:

package com.shellmonger.weatherapp.models;

import com.google.gson.Gson;

public class WeatherResponse {
    @Override
    public String toString() {
        return new Gson().toJson(this);
    }

    public static WeatherResponse fromJson(String json) {
        return new Gson().fromJson(json, WeatherResponse.class);
    }
}

Now that I have the conversion capabilities, I can add fields to my model. The Weather API is a complex model with multiple sections and embedded objects. Each embedded object becomes an embedded class in the model. Each field should have a SerializedName annotation to show what it will be in the JSON string. I add a SerializedName to every single field that must appear in the JSON output, even if the field is named the same. This ensures the right output should I change the internal field name. As an example, the model for the WeatherResponse looks like this:

package com.shellmonger.weatherapp.models;

import java.util.ArrayList;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;

/**
 * Model for describing the response from the OpenWeatherMap API
 */
public class WeatherResponse {
    @SerializedName("coord") private Coord coord;
    @SerializedName("weather") private ArrayList<Weather> weather;
    @SerializedName("base") private String base;
    @SerializedName("main") private Main main;
    @SerializedName("visibility") private int visibility;
    @SerializedName("wind") private Wind wind;
    @SerializedName("clouds") private Clouds clouds;
    @SerializedName("rain") private Precipitation rain;
    @SerializedName("snow") private Precipitation snow;
    @SerializedName("dt") private long unixTimestamp;
    @SerializedName("sys") private Sys sys;
    @SerializedName("id") private long city_id;
    @SerializedName("name") private String city_name;
    @SerializedName("cod") private int response_code;

    /**
     * Convert the current object to JSON string format
     * @return a JSON representation of the object
     */
    @Override
    public String toString() {
        return new Gson().toJson(this);
    }

    /**
     * Creates a new WeatherResponse object from a JSON string
     * @param json the JSON string needed to deserialize
     * @return a WeatherResponse object
     */
    public static WeatherResponse fromJson(String json) {
        return new Gson().fromJson(json, WeatherResponse.class);
    }

    /**
     * The internal representation of the "clouds" portion of the API
     */
    public class Clouds {
        @SerializedName("all") private int cloudiness;
    }

    /**
     * The internal representation of the "coord" portion of the API
     */
    public class Coord {
        @SerializedName("lon") private double lon;
        @SerializedName("lat") private double lat;
    }

    /**
     * The internal representation of the "main" portion of the API
     */
    public class Main {
        @SerializedName("temp") private double temp;
        @SerializedName("pressure") private int pressure;
        @SerializedName("humidity") private int humidity;
        @SerializedName("temp_min") private double temp_min;
        @SerializedName("temp_max") private double temp_max;
        @SerializedName("sea_level") private int sea_level;
        @SerializedName("grnd_level") private int ground_level;
    }

    /**
     * The internal representation of the "rain" and "snow" portion of the API
     */
    public class Precipitation {
        @SerializedName("3h") private double volume;
    }

    /**
     * The internal representation of the "sys" portion of the API
     */
    public class Sys {
        @SerializedName("type") private int type;
        @SerializedName("id") private int id;
        @SerializedName("message") private double message;
        @SerializedName("country") private String country;
        @SerializedName("sunrise") private long sunrise;
        @SerializedName("sunset") private long sunset;
    }

    /**
     * The internal representation of the "weather" list portion of the API
     */
    public class Weather {
        @SerializedName("id") private int id;
        @SerializedName("main") private String main;
        @SerializedName("description") private String description;
        @SerializedName("icon") private String icon;
    }

    /**
     * The internal representation of the "wind" portion of the API
     */
    public class Wind {
        @SerializedName("speed") private double speed;
        @SerializedName("deg") private int direction;
    }
}

At this point, you will be able to encode and decode JSON, but you won’t be able to access any fields within the model. For that, you have to make some decisions:

  1. How do you want to access the information in the model?
  2. Are there any fields that need special attention?

Let’s start with the access problem. You can access the fields two possible ways:

// Using properties directly
int pressure = response.main.pressure;

// Using getter/setters
int icon = response.getMain().getPressure();

There are a couple of concerns here. The JSON deserialization will work irrespective of whether you decide on public fields or getters/setters. I want my API to be read-only. There is no way to make a property read-only without a getter (with a lack of setter). There are other reasons for using getters/setters as well:

  • You want to convert a field that needs special attention.
  • You want to insulate the details of the model and only expose certain members.
  • When using a setter, you want to perform validation.

I find using properties directly is more readable, but Java aficionados will no doubt tell me that implementing getters and setters is the best practice. To use the properties directly, just change them from private to public. Defining the getter/setter is easy:

    public Coord getCoordinates() { return coord; }

    /**
     * Returns the array of weather conditions
     */
    public Weather[] getWeatherConditions() {
        Weather[] arr = new Weather[weather.size()];
        return weather.toArray(arr); 
    }
    
    public Main getMain() { return main; }
    
    public int getVisibility() { return visibility; }
    
    public Wind getWind() { return wind; }
    
    public Clouds getClouds() { return clouds; }
    
    public Precipitation getRain() { return rain; }
    
    public Precipitation getSnow() { return snow; }

    /**
     * Returns java.util.Date representation of the timestamp
     */
    public Date getTimestamp() {
        return new Date((long)(unixTimestamp * 1000));
    }
    
    public Sys getSysInfo() { return sys; }
    
    public long getCityId() { return city_id; }
    
    public String getCityName() { return city_name; }
    
    public int getResponseCode() { return response_code; }

Firstly, note that I did not expose everything. The “base” field is described as an “internal parameter”, so I didn’t include it. Also, note that I have two fields that got special attention. The first is the “weather” property. This is stored internally as a collection (specifically, an ArrayList object), but I want to expose it as an array. Secondly, the timestamp is transmitted as a unix timestamp in UTC (number of seconds since Jan 1, 1970). The normal way of representing a date in Java is to use a java.util.Date. The Date constructor takes milliseconds, but the UNIX timestamp is measured in seconds. It’s sometimes a good idea to return the data two different ways. For example, the icon has a set of “official” PNG files that you can download – you may want to convert the icon code into the appropriate icon location. Finally, don’t mix-and-match access methods. If you use getters, then use getters everywhere. Similarly, if you use property access, use that everywhere.

Testing

Android Studio has built-in unit testing facilities, and you should test your code. The OpenWeatherMap API has a bunch of examples right on the API page, so I use those to test my code. Here is an example test:

package com.shellmonger.weatherapp.models;

import org.junit.Test;
import static org.junit.Assert.*;

public class WeatherResponseTest {
    @Test
    public void deserializationWorks() {
        String input = "{\n" +
                "  \"coord\": {\n" +
                "    \"lon\": -0.13,\n" +
                "    \"lat\": 51.51\n" +
                "  },\n" +
                "  \"weather\": [\n" +
                "    {\n" +
                "      \"id\": 300,\n" +
                "      \"main\": \"Drizzle\",\n" +
                "      \"description\": \"light intensity drizzle\",\n" +
                "      \"icon\": \"09d\"\n" +
                "    }\n" +
                "  ],\n" +
                "  \"base\": \"stations\",\n" +
                "  \"main\": {\n" +
                "    \"temp\": 280.32,\n" +
                "    \"pressure\": 1012,\n" +
                "    \"humidity\": 81,\n" +
                "    \"temp_min\": 279.15,\n" +
                "    \"temp_max\": 281.15\n" +
                "  },\n" +
                "  \"visibility\": 10000,\n" +
                "  \"wind\": {\n" +
                "    \"speed\": 4.1,\n" +
                "    \"deg\": 80\n" +
                "  },\n" +
                "  \"clouds\": {\n" +
                "    \"all\": 90\n" +
                "  },\n" +
                "  \"dt\": 1485789600,\n" +
                "  \"sys\": {\n" +
                "    \"type\": 1,\n" +
                "    \"id\": 5091,\n" +
                "    \"message\": 0.0103,\n" +
                "    \"country\": \"GB\",\n" +
                "    \"sunrise\": 1485762037,\n" +
                "    \"sunset\": 1485794875\n" +
                "  },\n" +
                "  \"id\": 2643743,\n" +
                "  \"name\": \"London\",\n" +
                "  \"cod\": 200\n" +
                "}";
        WeatherResponse response = WeatherResponse.fromJson(input);

        // Check the output
        assertEquals(51.51, response.getCoordinates().getLatitude(), 0.001);
        assertEquals(-0.13, response.getCoordinates().getLongitude(), 0.001);

        assertEquals(1, response.getWeatherConditions().length);
        assertEquals(300, response.getWeatherConditions()[0].getId());
        assertEquals("Drizzle", response.getWeatherConditions()[0].getShortDescription());
        assertEquals("Light intensity drizzle", response.getWeatherConditions()[0].getLongDescription());
        assertEquals("09d", response.getWeatherConditions()[0].getIcon());

        assertEquals(280.32, response.getMain().getTemperature().getCurrentValue(), 0.001);
        assertEquals(279.15, response.getMain().getTemperature().getMinimumValue(), 0.001);
        assertEquals(281.15, response.getMain().getTemperature().getMaximumValue(), 0.001);
        assertEquals(1012, response.getMain().getPressure().getCurrentPressure());
        assertEquals(81, response.getMain().getHumidity());

        assertEquals(4.1, response.getWind().getSpeed(), 0.001);
        assertEquals(80, response.getWind().getDirection());

        assertEquals(90, response.getClouds().getCloudiness());

        assertEquals("GB", response.getSysInfo().getCountry());

        assertEquals(10000, response.getVisibility());
        assertEquals("London", response.getCityName());
        assertEquals(2643743, response.getCityId());
        assertEquals(200, response.getResponseCode());
    }
}

Android Studio gives you a couple of nice touches. Firstly, you can type String input="", then put your cursor in the middle of the quotes and paste the multiline JSON string into it. Android Studio will turn it into a multi-line string automatically for you, complete with all encoding. Secondly, there is a test runner built in. My tests are located in the com.shellmonger.weatherapp.models (test) folder. I can right-click on that folder and select Run Tests in ‘models’. Now, there is a bug in the test (so I can show the response):

The line number and the comparison that failes is shown. In addition, the test case is hyperlinked so you can easily open it in the editor. In this case, I’ve deliberately mistyped the response (a case mismatch), so I need to adjust the test. However, if I wanted to, I could set a breakpoint and then use Debug Tests in ‘models’ to run the tests in debug mode. This allows me to properly debug, correct and re-test quickly.

If I can give a piece of advice – test the encoding and decoding of your models with unit tests and sample data. When you do this, you will be much more confident that the model matches the reality of the API response you are trying to model.

My final model and the test associated with it is available on my GitHub repository.