Kotlin JSON Serialization using kotlinx.serialization

mahbaleshwar hegde(maabu)
4 min readMar 28, 2021
Photo by Marc Reichelt on Unsplash

I have been working on android app development for the last 1.5 years. I have to use parsing libraries to encode and decode JSON objects. There are many popular libraries like GSON, Moshi to serialize and deserialize objects. I majorly used GSON in my projects. But there are some limitations when we use GSON library with Kotlin.

Let’s take a look at below example

import com.google.gson.Gson

data class User(val firstName: String,
val lastName: String = "Appleased") {

val fullName: String
get() = "$firstName $lastName"
}

fun main() {
val json = """
{
"firstName": "John"
}
""".trimIndent()
val user = Gson().fromJson(json, User::class.java)
println(user.fullName)
print(user.lastName.isBlank())
}
Output: John null.
App crashes.

We have lost 2 major features of kotlin:

  1. Type Safety- lastname property is non nullable but GSON still can parse null string to create a user object.
  2. Argument’s default value has no effect.

You can find similar kinds of other problems and workaround solutions over the internet. Kotlin team has come up with the native support library kotlinx.serialization. This library provides supports for all supported platforms — JVM, JavaScript, Native — and for various serialization formats — JSON, CBOR, protocol buffers, and others.

Get Started with kotlinx.serialization

As I mentioned earlier kotlinx.serialization includes libraries for various serialization formats like JSON, Protocol buffers, CBOR, Properties, HOCON. This article covers only on kotlinx.serialization JSON serialization basics.

Installation

//use your project kotlin version.
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.4.30'
id 'org.jetbrains.kotlin.jvm' version '1.4.30'
}
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-serialization- json:1.1.0"
}

JSON Encoding

First, make a class serializable by annotating it with @Serializable

@Serializable
data class Project(val name: String, val language: String)

fun main() {
val project = Project("kotlinx.serialization", "Kotlin")
val jsonString = Json.encodeToString(project)
print(jsonString)
}
Output: {"name":"kotlinx.serialization","language":"Kotlin"}

JSON Decoding

@Serializable
data class Project(val name: String, val language: String)

fun main() {
val jsonString = """
{"name":"kotlinx.serialization","language":"Kotlin"}
""".trimIndent()
val project: Project = Json.decodeFromString(jsonString)
print(project)
}
Output: Project(name=kotlinx.serialization, language=Kotlin)

Lets dive into few important features of kotlinx.serialization.

1. Type Safety Enforced

kotlinx.serialization API ensures type safety. You cannot create an object from a null value where the constructor parameter expects a non-nullable value.

@Serializable
data class Project(val name: String, val language: String)

fun main() {
val jsonString = """
{"name":"kotlinx.serialization", "language": null}
""".trimIndent()
val project: Project = Json.decodeFromString(jsonString)
print(project)
}
Output: Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 45: Expected string literal but 'null' literal was found.

2. Supports default value in JSON decoding

kotlinx.serialization API supports default value of kotlin when decoding JSON string. You must use Json builder API and set coerceInputValuesto true.

@Serializable
data class Project(val name: String, val language: String = "kotlin")

fun main() {
val json = Json {
coerceInputValues = true
}
val jsonString = """
{"name":"kotlinx.serialization", "language": null}
""".trimIndent()
val project: Project = json.decodeFromString(jsonString)
print(project)
}
Output: Project(name=kotlinx.serialization, language=kotlin)

3. Generic classes

kotlinx.serialization API encode and decodes generic class type very easily and efficiently.
Encoding:

@Serializable
data class Version(val major: String,
val minor: String,
val patch: String)
@Serializable
data class Number<T>(val value: T)
@Serializable
data class Data(val intNumber: Number<Int>,
val longNumber: Number<Long>,
val versionNumber: Number<Version>)

fun main() {
val data = Data(Number(10),
Number(10L),
Number(Version("1", "0", "0")))
val encodedString = Json.encodeToString(data)
print(encodedString)
}
Output: {"intNumber":{"value":10},"longNumber":{"value":10},"versionNumber":{"value":{"major":"1","minor":"0","patch":"0"}}}

Decoding:

@Serializable
data class Project(val name: String, val language: String)
fun main() {

val jsonString = """
[
{
"name": "kotlinx.serialization",
"language": "kotlin"
},
{
"name": "coroutines",
"language": "kotlin"
}
]
""".trimIndent()
val projects: List<Project> = Json.decodeFromString(jsonString)
print(projects)
}
Output: [Project(name=kotlinx.serialization, language=kotlin), Project(name=coroutines, language=kotlin)]

We have not used any anonymous TypeToken object to get the generic type of converting object like in GSON or any Java based libraries. So no anonymous TypeToken object is required to decode the Generic kotlin types in kotlinx.serialization 😎.

4. Serial field names

There are often cases when Json key is different from their property name. we can add annotate property name with @SerialName("json_key")

@Serializable
data class Project(val name: String,
@SerialName("lang) val language: String)

5. Referenced objects

kotlinx.serialization supports nested object serialization. It only serializes property or class which are annotated as@Serializable, otherwise compiler throws the error:

@Serializable
data class Project(val name: String,
val language: String,
val project: Version
)
@Serializable // if you remove annotation compiler throws error.
data class Version(val major: Int,
val minor: Int,
val patch: Int)
Output: {"name":"Jetpack","language":"Kotlin","project":{"major":1,"minor":0,"patch":0}}

6. Data Validation

You can validate the data at the time of decoding json.

@Serializable
data class LoginResponse(val accessToken: String) {
init {
require(accessToken.isNotEmpty()) { "Access token cannot be empty" }
}
}

fun main() {

val jsonString = """
{ "accessToken": "" }
""".trimIndent()
val loginResponse: LoginResponse = Json.decodeFromString(jsonString)
}
Output: Exception in thread "main" java.lang.IllegalArgumentException: Access token cannot be empty

Retrofit support

You can check the awesome library for Retrofit 2 Converter.Factory for Kotlin serialization

Final Thoughts

kotlinx.serialization has a lot of good features. I have covered only the basics in this article. You can explore more features in their official documentation.

Happy coding ❤️😁

--

--