I just announced the new Spring 5 modules in REST With Spring:

>> CHECK OUT THE COURSE

1. Overview

Klaxon is one of the open source libraries that we can use to parse JSON in Kotlin.

In this tutorial, we’re going to look at its features.

2. Maven Dependency

First, we’ll need to add the library dependency to our Maven project:

<dependency>
    <groupId>com.beust</groupId>
    <artifactId>klaxon</artifactId>
    <version>3.0.4</version>
</dependency>

The latest version can be found at jcenter or in the Spring Plugins Repository.

3. API Features

Klaxon has four APIs to work with JSON documents. We’ll explore these in the following sections.

4. Object Binding API

With this API, we can bind JSON documents to Kotlin objects and vice-versa.
To start, let’s define the following JSON document:

{
    "name": "HDD"
}

Next, we’ll create the Product class for binding:

class Product(val name: String)

Now, we can test serialization:

@Test
fun givenProduct_whenSerialize_thenGetJsonString() {
    val product = Product("HDD")
    val result = Klaxon().toJsonString(product)

    assertThat(result).isEqualTo("""{"name" : "HDD"}""")
}

And we can test deserialization:

@Test
fun givenJsonString_whenDeserialize_thenGetProduct() {
    val result = Klaxon().parse<Product>(
    """
        {
            "name" : "RAM"
        }
    """)

    assertThat(result?.name).isEqualTo("RAM")
}

This API also supports working with data classes as well mutable and immutable classes.

Klaxon allows us to customize the mapping process with the @Json annotation. This annotation has two properties:

  • name – for setting a different name for the fields
  • ignored – for ignoring fields of the mapping process

Let’s create a CustomProduct class to see how these work:

class CustomProduct(
    @Json(name = "productName")
    val name: String,
    @Json(ignored = true)
    val id: Int)

Now, let’s verify it with a test:

@Test
fun givenCustomProduct_whenSerialize_thenGetJsonString() {
    val product = CustomProduct("HDD", 1)
    val result = Klaxon().toJsonString(product)

    assertThat(result).isEqualTo("""{"productName" : "HDD"}""")
}

As we can see, the name property is serialized as productName, and the id property is ignored.

5. Streaming API

With the Streaming API, we can handle huge JSON documents by reading from a stream. This feature allows our code to process JSON values while it is still reading.

We need to use the JsonReader class from the API to read a JSON stream. This class has two special functions to handle streaming:

  • beginObject() – makes sure that the next token is the beginning of an object
  • beginArray() – makes sure that the next token is the beginning of an array

With these functions, we can be sure the stream is correctly positioned and that it’s closed after consuming the object or array.

Let’s test the streaming API against an array of the following ProductData class:

data class ProductData(val name: String, val capacityInGb: Int)
@Test
fun givenJsonArray_whenStreaming_thenGetProductArray() {
    val jsonArray = """
    [
        { "name" : "HDD", "capacityInGb" : 512 },
        { "name" : "RAM", "capacityInGb" : 16 }
    ]"""
    val expectedArray = arrayListOf(
      ProductData("HDD", 512),
      ProductData("RAM", 16))
    val klaxon = Klaxon()
    val productArray = arrayListOf<ProductData>()
    JsonReader(StringReader(jsonArray)).use { 
        reader -> reader.beginArray {
            while (reader.hasNext()) {
                val product = klaxon.parse<ProductData>(reader)
                productArray.add(product!!)
            }
        }
    }

    assertThat(productArray).hasSize(2).isEqualTo(expectedArray)
}

6. JSON Path Query API

Klaxon supports the element location feature from the JSON Path specification. With this API, we can define path matchers to locate specific entries in our documents.

Note that this API is streaming, too, and we’ll be notified after an element is found and parsed.

We need to use the PathMatcher interface. This interface is called when the JSON Path found matches of the regular expression.

To use this, we need to implement its methods:

  • pathMatches() – return true if we want to observe this path
  • onMatch() – fired when the path is found; note that the value can only be a basic type (e.g., int, String) and never JsonObject or JsonArray

Let’s make a test to see it in action.

First, let’s define an inventory JSON document as a source of data:

{
    "inventory" : {
        "disks" : [
            {
                "type" : "HDD",
                "sizeInGb" : 1000
            },
            {
                "type" : "SDD",
                "sizeInGb" : 512
            }
        ]
    }
}

Now, we implement the PathMatcher interface as follows:

val pathMatcher = object : PathMatcher {
    override fun pathMatches(path: String)
      = Pattern.matches(".*inventory.*disks.*type.*", path)

    override fun onMatch(path: String, value: Any) {
        when (path) {
            "$.inventory.disks[0].type"
              -> assertThat(value).isEqualTo("HDD")
            "$.inventory.disks[1].type"
              -> assertThat(value).isEqualTo("SDD")
        }
    }
}

Note we defined the regex to match the type of disk of our inventory document.

Now, we are ready to define our test:

@Test
fun givenDiskInventory_whenRegexMatches_thenGetTypes() {
    val jsonString = """..."""
    val pathMatcher = //...
    Klaxon().pathMatcher(pathMatcher)
      .parseJsonObject(StringReader(jsonString))
}

7. Low-Level API

With Klaxon, we can process JSON documents like a Map or a List. To do this, we can use the classes JsonObject and JsonArray from the API.

Let’s make a test to see the JsonObject in action:

@Test
fun givenJsonString_whenParser_thenGetJsonObject() {
    val jsonString = StringBuilder("""
        {
            "name" : "HDD",
            "capacityInGb" : 512,
            "sizeInInch" : 2.5
        }
    """)
    val parser = Parser()
    val json = parser.parse(jsonString) as JsonObject

    assertThat(json)
      .hasSize(3)
      .containsEntry("name", "HDD")
      .containsEntry("capacityInGb", 512)
      .containsEntry("sizeInInch", 2.5)
}

Now, let’s make a test to see the JsonArray functionality:

@Test
fun givenJsonStringArray_whenParser_thenGetJsonArray() {
    val jsonString = StringBuilder("""
    [
        { "name" : "SDD" },
        { "madeIn" : "Taiwan" },
        { "warrantyInYears" : 5 }
    ]""")
    val parser = Parser()
    val json = parser.parse(jsonString) as JsonArray<JsonObject>

    assertSoftly({
        softly ->
            softly.assertThat(json).hasSize(3)
            softly.assertThat(json[0]["name"]).isEqualTo("SDD")
            softly.assertThat(json[1]["madeIn"]).isEqualTo("Taiwan")
            softly.assertThat(json[2]["warrantyInYears"]).isEqualTo(5)
    })
}

As we can see in both cases, we made the conversions without the definition of specific classes.

8. Conclusion

In this article, we explored the Klaxon library and its APIs to handle JSON documents.

As always, the source code is available over on Github.

I just announced the new Spring 5 modules in REST With Spring:

>> CHECK OUT THE LESSONS