Categories
Kotlin

Create JSON manually with kotlinx.serialization

Kotlin serialization is a great library for serialisation in Kotlin. It is mainly geared towards serialising from objects to strings and back, but on closer look it also contains a comprehensive Json library. Even after discovering the documentation, though, the use of this new library might be confusing.

๐Ÿ•‹ Serialisation to objects

Consider a data class:

@Serializable
data class Credentials(
	val publicKey: String,
	val privateKey: String,
)

The @Serializable annotation enables the encoding to string and back:

val credentials = Credentials("publicKey", "privateKey")

val stringValue = Json.encodeToString(credentials)
println(stringValue)

val credentialsDecoded = Json.decodeFromString<Credentials>(stringValue)
println(credentialsDecoded.publicKey)

/* 
output:
	{"publicKey":"publicKey","privateKey":"privateKey"}
	Credentials(publicKey=publicKey, privateKey=privateKey)
*/

If there is no need for a data class, the json string can also be decoded straight into a JsonObject

val jsonObject = Json.decodeFromString<JsonObject>(stringValue)

With object serialisation, the the library shines with its ease of use. With JSON, however, the use becomes more ambiguous.

๐ŸŽž Serialisation to JSON string, manually

When creating a web requests, a separate class for posting the data is not required. Then, the request body can be created with the JSON features part of the serialisation library. In there, comprehensive function set exists to handle most JSON encoding problems.

Creating our credentials string, for example, would look like:

val credentials = JsonObject(
    mapOf(
        "publicKey" to JsonPrimitive("publicKey"),
        "privateKey" to JsonPrimitive("privateKey")
    )
)

val array = JsonArray(listOf(credentials))
println(array.toString())

/* 
output:
	[{"publicKey":"publicKey","privateKey":"privateKey"}]
*/

There is also a DSL version of this construction, which might be preferred:


val credentials = buildJsonArray {
    addJsonObject {
        put("publicKey", "publickey")
        put("privateKey", "privateKey")
    }
}

println(credentials.toString())

/*
output:
	[{"publicKey":"publickey","privateKey":"privateKey"}]
*/

How to create a JsonObject from a map<*, *>?

For a map with generic keys and values, an extension method can be defined that checks and converts the primitives to a JsonObject.

fun Map<*, *>.toJsonElement(): JsonElement {
      val map: MutableMap = mutableMapOf()
      this.forEach {
          val key = it.key as? String ?: return@forEach
          val value = it.value ?: return@forEach
          when (value) {
              // convert containers into corresponding Json containers
              is Map<*, *> -> map[key] = (value).toJsonElement()
              is List<*> -> map[key] = value.toJsonElement()
              // convert the value to a JsonPrimitive
              else -> map[key] = JsonPrimitive(value.toString())
          }
      }
      return JsonObject(map)
  }

How to create a JsonObject from a List<*>?

For converting from a list, and extension method can also be defined.

fun List<*>.toJsonElement(): JsonElement {
val list: MutableList = mutableListOf()

fun List<*>.toJsonElement(): JsonElement {
    val list: MutableList = mutableListOf()

    this.forEach {
        val value = it as? Any ?: return@forEach
        when (value) {
            is Map<*, *> -> list.add((value).toJsonElement())
            is List<*> -> list.add(value.toJsonElement())
            else -> list.add(JsonPrimitive(value.toString()))
        }
    }

    return JsonArray(list)
}

๐ŸŒ Serialisation for YAML and other formats

Only JSON JSON, and some experimental formats, are supported out of the box. For others, like YAML, an external library that implements a custom formatter, can be used.

With this dependency, a YAML string can decoded into a @Serializable object as follows:

val yamlEncoded =
    """
        publicKey: "publicKey"
        privateKey: "privateKey"
    """.trimIndent()

val credentials =
    Yaml.default.decodeFromString(
        Credentials.serializer(),
        yamlEncoded
    )

println(credentials)

/*
output:
	Credentials(publicKey=publicKey, privateKey=privateKey)
*/

๐ŸŒธ Pretty printing a JSON string

If JSON input is without line breaks, it can be useful to make it more human readable. The JSON library can then be utilised with the prettyPrint property.

val format = Json { prettyPrint = true }
val input = """
    {"publicKey":"publicKey","privateKey":"privateKey"}
""".trimIndent()

val jsonElement = format.decodeFromString<JsonElement>(input)
val bodyInPrettyPrint = format.encodeToString(jsonElement)

println(bodyInPrettyPrint)
/*
output:
    {
        "publicKey": "publicKey",
        "privateKey": "privateKey"
    }
*/

As shown here, the input string needs to be decoded into a JsonElement, and then encoded back to string again. Only then, the prettyPrint property will cooperate in achieving our goal.

โŒ›๏ธ Conclusion

Kotlinx.serialization is a great tool for serialising objects and parsing JSON strings. Since it is a new library within a new language, all of the features might not be obvious at first. Therefore, analysis of the documentation is encouraged before using it in code.

Sample code is available in tonisives repo.

Related tweet: