JSON to Kotlin data class example
With Profile as the Root name, the sample user profile becomes:
{
"userId": "u-7781",
"displayName": "Noor",
"avatar": {
"url": "https://cdn.example.com/a/7781.png",
"width": 256,
"height": 256
},
"settings": {
"darkMode": true,
"language": "ar",
"notifications": { "push": true, "email": false }
},
"badges": ["early-adopter", "reviewer"]
}
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Profile(
@SerialName("userId") val userId: String,
@SerialName("displayName") val displayName: String,
@SerialName("avatar") val avatar: Avatar,
@SerialName("settings") val settings: Settings,
@SerialName("badges") val badges: List<String>,
)
@Serializable
data class Settings(
@SerialName("darkMode") val darkMode: Boolean,
@SerialName("language") val language: String,
@SerialName("notifications") val notifications: Notifications,
)
@Serializable
data class Notifications(
@SerialName("push") val push: Boolean,
@SerialName("email") val email: Boolean,
)
@Serializable
data class Avatar(
@SerialName("url") val url: String,
@SerialName("width") val width: Long,
@SerialName("height") val height: Long,
)
Each object is a data class with val properties, so instances are immutable and compare by value. Whole numbers are Long, since a sample can’t show whether a value will outgrow Int, and a value too large for Int fails to decode. @SerialName sits on every property, even where it matches the key, so you can rename userId to id without breaking decoding. It also maps a snake_case key such as avatar_url to the camelCase property avatarUrl.
Nullable, optional and mixed values
Array items are merged before typing (rules on JSON to TypeScript). With Results as the root name:
[
{ "player": "noor", "score": 1200 },
{ "player": "kai", "score": "DNF", "team": "red" }
]
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
@Serializable
data class Result(
@SerialName("player") val player: String,
@SerialName("score") val score: JsonElement,
@SerialName("team") val team: String? = null,
)
team is missing from the first item, so it is String? = null. The default is what makes the key optional: kotlinx.serialization requires every property without a default, even a nullable one. score was a number in one item and a string in the other, so it is a JsonElement; read it with score.jsonPrimitive.content, or .longOrNull for the number.
Setup and decoding
Apply the serialization compiler plugin (kotlin("plugin.serialization") in Gradle, at your Kotlin version) and add org.jetbrains.kotlinx:kotlinx-serialization-json. Without the plugin, @Serializable does nothing and decoding fails at runtime for lack of a serializer.
import kotlinx.serialization.*
import kotlinx.serialization.json.*
val json = Json { ignoreUnknownKeys = true }
fun parseProfile(body: String): Profile = json.decodeFromString<Profile>(body)
The default Json throws on undeclared keys, so ignoreUnknownKeys = true keeps older app versions working when the API adds a field. When encoding, properties still at their null default are left out unless you set encodeDefaults = true. The same json instance plugs into Ktor’s ContentNegotiation plugin and Retrofit’s kotlinx.serialization converter.
For the iOS and Flutter sides, JSON to Swift and JSON to Dart build matching models.