JSON Parsing in Kotlin — Parsing with Kotlin Serialization
Parse a complex JSON response using Kotlin Serialization — the Kotlin-native, compile-time safe approach with no reflection.
This is Part 4 of the JSON Parsing in Kotlin series. This is the one I’ve been building up to — Kotlin Serialization is the Kotlin-native approach to JSON parsing. No Java legacy, no reflection, compile-time safety. If you’re writing Kotlin, this is the library that was built for you.
The JSON
Throughout this series, we’re parsing the same JSON — a typical SaaS API response with users, subscriptions, teams, and pagination. It has nested objects, arrays, nullable fields, enums, dates, and decimals.
{
"status": "success",
"data": {
"users": [
{
"id": "usr_a1b2c3d4",
"name": "Navkrishna",
"email": "[email protected]",
"role": "ADMIN",
"verified": true,
"createdAt": "2026-01-15T10:30:00Z",
"profile": {
"avatar": "https://cdn.example.com/avatars/nav.jpg",
"bio": "Full-stack developer",
"social": {
"github": "krrishnaaaa",
"twitter": null,
"linkedin": "navkrishna"
}
},
"subscription": {
"plan": "PRO",
"billingCycle": "YEARLY",
"price": 199.99,
"features": ["analytics", "api_access", "priority_support"],
"trialEndsAt": null
},
"teams": [
{
"id": "team_x1y2",
"name": "Backend",
"role": "OWNER",
"memberCount": 5
},
{
"id": "team_z3w4",
"name": "Mobile",
"role": "MEMBER",
"memberCount": 3
}
],
"lastLoginAt": "2026-03-14T18:45:00Z"
},
{
"id": "usr_e5f6g7h8",
"name": "Priya Sharma",
"email": "[email protected]",
"role": "MEMBER",
"verified": false,
"createdAt": "2026-03-01T14:00:00Z",
"profile": {
"avatar": null,
"bio": null,
"social": {
"github": "priya-dev",
"twitter": "priyacodes",
"linkedin": null
}
},
"subscription": {
"plan": "TRIAL",
"billingCycle": null,
"price": 0.0,
"features": ["analytics"],
"trialEndsAt": "2026-03-31T23:59:59Z"
},
"teams": [],
"lastLoginAt": null
}
],
"pagination": {
"page": 1,
"perPage": 20,
"totalItems": 2,
"totalPages": 1,
"hasNext": false
}
},
"meta": {
"requestId": "req_9k8j7h6g",
"timestamp": "2026-03-15T00:00:00Z",
"apiVersion": "v2"
}
}
Setup
Kotlin Serialization requires both a Gradle plugin and a runtime library. In your build.gradle.kts:
plugins {
kotlin("jvm") version "2.1.10"
kotlin("plugin.serialization") version "2.1.10"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
}
The plugin runs at compile time and generates serializers for every class annotated with @Serializable. No reflection, no runtime surprises — if it compiles, it serializes.
Model Classes
Here’s where Kotlin Serialization shines. You annotate your data classes with @Serializable, and the compiler does the rest. For fields where the JSON key doesn’t match the Kotlin property name, use @SerialName.
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Social(
val github: String?,
val twitter: String?,
val linkedin: String?
)
@Serializable
data class Profile(
val avatar: String?,
val bio: String?,
val social: Social
)
@Serializable
data class Subscription(
val plan: String,
val billingCycle: String?,
val price: Double,
val features: List<String>,
val trialEndsAt: String?
)
@Serializable
data class Team(
val id: String,
val name: String,
val role: String,
val memberCount: Int
)
@Serializable
data class User(
val id: String,
val name: String,
val email: String,
val role: String,
val verified: Boolean,
val createdAt: String,
val profile: Profile,
val subscription: Subscription,
val teams: List<Team>,
val lastLoginAt: String?
)
@Serializable
data class Pagination(
val page: Int,
val perPage: Int,
val totalItems: Int,
val totalPages: Int,
val hasNext: Boolean
)
@Serializable
data class Meta(
val requestId: String,
val timestamp: String,
val apiVersion: String
)
@Serializable
data class Data(
val users: List<User>,
val pagination: Pagination
)
@Serializable
data class ApiResponse(
val status: String,
val data: Data,
val meta: Meta
)
Notice: every nullable JSON field is String? in Kotlin. The serializer handles null automatically — no optString(), no isNull() checks, no surprises.
Handling Dates with a Custom Serializer
The model above keeps dates as String. That works, but we can do better. Let’s write a custom serializer for java.time.Instant:
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.Instant
object InstantSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): Instant {
return Instant.parse(decoder.decodeString())
}
}
Now update the date fields in the model classes to use it:
import kotlinx.serialization.Serializable
@Serializable
data class Subscription(
val plan: String,
val billingCycle: String?,
val price: Double,
val features: List<String>,
@Serializable(with = InstantSerializer::class)
val trialEndsAt: Instant?
)
@Serializable
data class User(
val id: String,
val name: String,
val email: String,
val role: String,
val verified: Boolean,
@Serializable(with = InstantSerializer::class)
val createdAt: Instant,
val profile: Profile,
val subscription: Subscription,
val teams: List<Team>,
@Serializable(with = InstantSerializer::class)
val lastLoginAt: Instant?
)
@Serializable
data class Meta(
val requestId: String,
@Serializable(with = InstantSerializer::class)
val timestamp: Instant,
val apiVersion: String
)
The @Serializable(with = ...) annotation tells the compiler exactly which serializer to use. No runtime registration, no global configuration — it’s all resolved at compile time.
Tip: If you’re targeting Kotlin Multiplatform, use
kotlinx.datetime.Instantinstead ofjava.time.Instant. Thekotlinx-datetimelibrary has built-in serialization support, so you don’t even need a custom serializer.
Parsing
Here’s the entire parsing code:
import kotlinx.serialization.json.Json
val json = Json {
ignoreUnknownKeys = true
}
fun parse(jsonString: String): ApiResponse {
return json.decodeFromString<ApiResponse>(jsonString)
}
That’s it. Three lines. The ignoreUnknownKeys = true configuration tells the parser to skip any JSON fields that don’t have a matching property in your data class. Without it, unknown fields throw an exception — which is actually a nice safety net during development.
Putting It Together
import java.io.File
fun main() {
val jsonString = File("response.json").readText()
val response = parse(jsonString)
println("Status: ${response.status}")
println("API Version: ${response.meta.apiVersion}")
println("Total users: ${response.data.pagination.totalItems}")
println()
for (user in response.data.users) {
println("--- ${user.name} ---")
println(" Email: ${user.email}")
println(" Role: ${user.role}")
println(" Verified: ${user.verified}")
println(" Plan: ${user.subscription.plan}")
println(" Price: $${user.subscription.price}")
println(" Features: ${user.subscription.features}")
println(" GitHub: ${user.profile.social.github}")
println(" Teams: ${user.teams.size}")
for (team in user.teams) {
println(" - ${team.name} (${team.role})")
}
println(" Last login: ${user.lastLoginAt}")
println()
}
}
Output
Status: success
API Version: v2
Total users: 2
--- Navkrishna ---
Email: [email protected]
Role: ADMIN
Verified: true
Plan: PRO
Price: $199.99
Features: [analytics, api_access, priority_support]
GitHub: krrishnaaaa
Teams: 2
- Backend (OWNER)
- Mobile (MEMBER)
Last login: 2026-03-14T18:45:00Z
--- Priya Sharma ---
Email: [email protected]
Role: MEMBER
Verified: false
Plan: TRIAL
Price: $0.0
Features: [analytics]
GitHub: priya-dev
Teams: 0
Last login: null
Why Kotlin Serialization is Different
This isn’t just “another JSON library.” Here’s what sets it apart:
| Feature | Kotlin Serialization | Reflection-based libraries |
|---|---|---|
| Code generation | Compile-time (compiler plugin) | Runtime reflection |
| Null safety | Enforced by Kotlin’s type system | Runtime errors |
| Unknown fields | Configurable (fail or ignore) | Usually silently ignored |
| Performance | No reflection overhead | Reflection cost on every parse |
| Multiplatform | JS, Native, Wasm, JVM | JVM only |
| Proguard/R8 | No special rules needed | Needs keep rules |
The compile-time safety is the killer feature. If you rename a field in your data class but forget to update @SerialName, the compiler catches it. If you mark a non-nullable field but the JSON sends null, you get a clear SerializationException instead of a silent null in a non-nullable slot.
Using in Android
Kotlin Serialization is the recommended approach for JSON parsing in Android, endorsed by Google. It’s lightweight, has no reflection (great for startup performance), and needs no Proguard rules.
// build.gradle.kts (app module)
plugins {
id("com.android.application")
kotlin("android")
kotlin("plugin.serialization") version "2.1.10"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
}
It also integrates cleanly with Retrofit via a converter:
// build.gradle.kts
dependencies {
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
}
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
val contentType = "application/json".toMediaType()
val json = Json { ignoreUnknownKeys = true }
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(json.asConverterFactory(contentType))
.build()
No Proguard rules, no reflection, fast startup. This is the modern Android stack.
What’s Next
In the next post, we’ll parse this same JSON using Jackson with the Kotlin module — the heavyweight champion of server-side Java that also plays nicely with Kotlin. If you’re in a Spring Boot world, that one’s for you.