Kotlin JSON Serialization using kotlinx.serialization
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:
- Type Safety-
lastname
property is non nullable but GSON still can parse null string to create a user object. - 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 coerceInputValues
to 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 ❤️😁