Sunday, June 18, 2017

Filtering and Querying JSON for Tests

When I'm developing an API, I write unit tests along the way to test smaller portions of my application. In addition to having unit tests, I like to add an integration test (or smoketest). The point of this integration test is to exercise the APIs endpoints as maybe a customer would.

I write a simple shell script that calls the endpoints and verifies responses. My goal is to create a smoketest script that is easy to use and maintain so I can quickly test out endpoints. This kind of script can also be used to test an application when an update has been deployed.

I'm experimenting with using curl and jq in my smoketest script to parse JSON, and I would like to share here a few fun ways to test responses from APIs.

The simplest way to use curl and jq together is to send a request with curl then format the json response with jq. The Cocktail DB has an nice API we can use for simple testing (please be nice and don't bombard their API with requests!). So let's try getting a random cocktail recipe:

curl http://www.thecocktaildb.com/api/json/v1/1/random.php

OK. Notice that it's hard to read the JSON response. Let's now take that same request and pipe the response output to jq like this:

curl http://www.thecocktaildb.com/api/json/v1/1/random.php | jq .

Wow - so much nicer! Using jq to reformat the response makes it easier to visually inspect the JSON.

Now let's check the HTTP response code that is returned. The option -w '%{http_code' will print just the HTTP response code. The rest of the output is redirected to /dev/null, and the result is stored in the shell variable STATUS:

STATUS=$(curl -so /dev/null -w '%{http_code}' ${COCKTAIL_DB_API}/random.php)

Next, let's count the number of Vodka drinks in the database. To do this, we're going to use the jq built-in function length. When you look at the JSON in the response, you see that the field drinks is an array of drinks. Here is the shell code that gets the number of Vodka drinks:

COUNT=$(curl -s ${COCKTAIL_DB_API}/filter.php\?i\=Vodka | jq '.drinks | length')

When I looked at the JSON response, I noticed that a drink has these fields:

  • strDrink
  • strDrinkThumb
  • idDrink
Some of the thumbnails are null. We can count the number of drinks with null thumbnails using jq's select. Select only the drinks where strDrinkThumb is null, create an array of those drinks, and get the length of the array. Here's how I did it in my shell script:

NULL_THUMBNAIL_COUNT=$(curl -s ${COCKTAIL_DB_API}/filter.php\?i\=Vodka | jq '[.drinks[] | select(.strDrinkThumb == null)] | length')

You might use this in a test to verify that no drinks have null thumbnails.

These examples are in the script cocktail-db-tests.sh in my repository https://github.com/annawinkler/api-tests.

While working out these examples,  I found this site helpful for debugging my jq filter https://jqplay.org/ and the jq manual a great reference https://stedolan.github.io/jq/manual/. Sometimes I need to diff json, and I like to use this online tool for that: http://www.jsondiff.com/.

Saturday, March 4, 2017

Serialize null values with jsonschema2pojo

Over the last week, I have encountered a number of questions and issues for which no easy answer could be found via Internet searches. Starting a blog has been on my mind for a while, so I decided today to take the plunge and write about my questions and problems, and the answers I find.

I use jsonschema2pojo, a nice library that takes JSON schema and generates Plain Old Java Objects (POJOs). The POJOs are used in my Java API as domain model objects. It's useful to generate this code via the JSON schema so future changes are made in one place (the JSON schema) versus two places (the JSON schema and the Java domain model objects). I build my project with maven, so I use the jsonschema2pojo maven plugin to generate the POJOs.

Here is the issue I was having with my JSON: I wanted to include (or serialize) a field both when it had a value and when it was null. So let's look at this problem with a JSON schema example of a playlist and a song.

Let's define a playlist as follows:
{
  "$schema": "http://json-schema.org/draft-04/hyper-schema#",
  "title": "playList",
  "type": "object",
  "description": "A list of songs",
  "javaType": "org.anna.testJsonSchema.models.v1.PlayList",
  "additionalProperties": false,
  "required": [
    "name",
    "songs"  ],
  "properties": {
    "id": {
      "description": "Server generated identifier for this playlist",
      "type": "string"    },
    "name": {
      "description": "The name of the playlist",
      "type": "string"    },
    "songs": {
      "description": "Songs in the playlist",
      "type": "array",
      "items": {
        "$ref": "song.json"      },
      "minItems": 1,
      "maxItems": 100,
      "uniqueItems": true    }
  }
}
And let's define a song like this:
{
  "$schema": "http://json-schema.org/draft-04/hyper-schema#",
  "title": "document",
  "type": "object",
  "description": "A song",
  "javaType": "org.anna.testJsonSchema.models.v1.Song",
  "additionalProperties": false,
  "required": [
    "artist",
    "title"  ],
  "properties": {
    "id": {
      "description": "Server generated id",
      "type": "string"    },
    "artist": {
      "description": "The song artist",
      "type": "string"    },
    "title": {
      "description": "The song title",
      "type": "string"    },
    "genre": {
      "description": "The song genre",
      "type": "string"    }
  }
}
In this example, a genre is not required, and let's say that we want the serialized object to either print the genre string or null when none is defined. (As an aside, a genre could be a nice place to use an enumerated type).

Now, the first time I generated the Java code for a Song, this is the resulting class (package, import, and comment statements removed - see what's checked into the github project):
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({
    "id",
    "artist",
    "title",
    "genre"})public class Song {
    @JsonProperty("id")
    @JsonPropertyDescription("Server generated id")
    private String id;
    @JsonProperty("artist")
    @JsonPropertyDescription("The song artist")
    @NotNull    private String artist;
    @JsonProperty("title")
    @JsonPropertyDescription("The song title")
    @NotNull    private String title;
    @JsonProperty("genre")
    @JsonPropertyDescription("The song genre")
    private String genre;
    @JsonProperty("id")
    public String getId() {
        return id;
    }
    @JsonProperty("id")
    public void setId(String id) {
        this.id = id;
    }

    public Song withId(String id) {
        this.id = id;
        return this;
    }
    @JsonProperty("artist")
    public String getArtist() {
        return artist;
    }    @JsonProperty("artist")
    public void setArtist(String artist) {
        this.artist = artist;
    }

    public Song withArtist(String artist) {
        this.artist = artist;
        return this;
    }
    @JsonProperty("title")
    public String getTitle() {
        return title;
    }
    @JsonProperty("title")
    public void setTitle(String title) {
        this.title = title;
    }

    public Song withTitle(String title) {
        this.title = title;
        return this;
    }
    @JsonProperty("genre")
    public String getGenre() {
        return genre;
    }
    @JsonProperty("genre")
    public void setGenre(String genre) {
        this.genre = genre;
    }

    public Song withGenre(String genre) {
        this.genre = genre;
        return this;
    }

    @Override    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }

    @Override    public int hashCode() {
        return new HashCodeBuilder().append(id)
                   .append(artist).append(title)
                   .append(genre).toHashCode();
    }

    @Override    public boolean equals(Object other) {
        if (other == this) {
            return true;
        }
        if ((other instanceof Song) == false) {
            return false;
        }
        Song rhs = ((Song) other);
        return new EqualsBuilder().append(id, rhs.id)
                                  .append(artist, rhs.artist)
                                  .append(title, rhs.title)
                                  .append(genre, rhs.genre).isEquals();
    }
}
I highlighted in yellow the line to look at carefully. That line means that no null values will be included in the JSON. Exactly what I don't want. How to change this?

I tried modifying the JSON schema a few different ways, none of which worked. First I tried making the type either string or null like this:
"genre": {
  "description": "The song genre",
  "type": ["string", "null"]
}
Next I tried using oneOf for the type like this, which generates an Object for genre in the Java code:
"genre": {
  "description": "The song genre",
   "oneOf": [
    {"type": "null"},
    {"type": "string"}
  ]}
Or even saying the field is nullable:
"genre": {
  "description": "The song genre",
  "type": "string",
  "nullable": true}
The problem is the Song class still has this annotation:
@JsonInclude(JsonInclude.Include.NON_NULL)
I finally discovered a configuration option for jsonschema2pojo that changes that JsonInclude: inclusionLevel!

So, the build section in my pom.xml now looks like this (I highlighted in yellow the inclusionLevel configuration option):
<build>
    <plugins>
        <plugin>
            <groupId>org.jsonschema2pojo</groupId>
            <artifactId>jsonschema2pojo-maven-plugin</artifactId>
            <version>0.4.30</version>
            <configuration>
                <sourceDirectory>${basedir}/src/main/resources/schemas/json/v1</sourceDirectory>
                <outputDirectory>${basedir}/src/main/java</outputDirectory>
                <targetPackage>org.anna.testJsonSchema.models.v1</targetPackage>
                <generateBuilders>true</generateBuilders>
                <includeJsr303Annotations>true</includeJsr303Annotations>
                <includeConstructors>false</includeConstructors>
                <includeAdditionalProperties>true</includeAdditionalProperties>
                <initializeCollections>true</initializeCollections>
                <targetVersion>42</targetVersion>
                <useCommonsLang3>true</useCommonsLang3>
                <inclusionLevel>ALWAYS</inclusionLevel>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>generate</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
Regenerating the Java objects, the Song class is now annotated with:
@JsonInclude(JsonInclude.Include.ALWAYS)
And we get null in the JSON! My work is done!

I'll be honest - this solution is not ideal if you don't want all fields in your class with null values to be included in the JSON. I don't know how to solve that issue. Maybe you do? Feel free to comment with ideas.

You can find this Java project in github.