Parse JSON in Kotlin
Kotlin has 3 main JSON libraries: kotlinx.serialization (JetBrains' official multiplatform library), Gson (Google's Java library), and Moshi (Square's Kotlin-idiomatic library). kotlinx.serialization is the recommended choice for new projects — it generates serializers at compile time via a Gradle plugin (no reflection), supports all Kotlin targets (JVM, JS, Native, Wasm), and handles Kotlin's null safety by mapping JSON null to Kotlin nullable types. For Android projects using Retrofit, Gson and Moshi are popular HTTP converter options. This guide covers all three with practical examples: parsing data classes, JSON arrays, nested objects, null handling, and error handling.
Validate your JSON before parsing it in Kotlin.
Open JSON Formatterkotlinx.serialization: the official Kotlin library
kotlinx.serialization is the official JetBrains JSON library for Kotlin. Unlike Gson, it uses a Gradle plugin to generate serializer code at compile time — no runtime reflection, full support for Kotlin Multiplatform, and tight integration with Kotlin's type system. The most important configuration option is ignoreUnknownKeys: by default, the library throws SerializationException if the JSON contains fields not present in your data class (intentionally strict to catch API drift early). Set ignoreUnknownKeys = true when consuming third-party APIs.
Gradle setup
// build.gradle.kts
plugins {
kotlin("jvm") version "2.0.0"
kotlin("plugin.serialization") version "2.0.0" // Gradle plugin (required)
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
}Basic parsing and serialization
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
// Annotate data classes with @Serializable
@Serializable
data class User(
val id: Int,
val name: String,
val email: String? = null, // nullable — maps to JSON null or absent key
val role: String = "user", // default value — used when key is absent
)
fun main() {
val jsonStr = """{"id": 1, "name": "Alice", "email": "alice@example.com"}"""
// Decode (parse) JSON → data class
val user: User = Json.decodeFromString(jsonStr)
println(user.name) // Alice
println(user.role) // user (default value applied)
// Encode (serialize) data class → JSON
val encoded = Json.encodeToString(user)
println(encoded)
// {"id":1,"name":"Alice","email":"alice@example.com"}
// Note: role is NOT included because encodeDefaults = false by default
}Configuring the Json instance
import kotlinx.serialization.json.Json
import kotlinx.serialization.decodeFromString
// Create a configured Json instance — do this once and reuse it
val json = Json {
ignoreUnknownKeys = true // do not throw on extra JSON fields (most important!)
isLenient = true // allow unquoted strings, relaxed JSON
encodeDefaults = true // include fields with default values in output
prettyPrint = true // human-readable indented output
coerceInputValues = true // coerce null to default value for non-nullable fields
}
@Serializable
data class Product(val id: Int, val name: String, val price: Double)
// Real API response may have extra fields — ignoreUnknownKeys handles this
val apiResponse = """
{
"id": 42,
"name": "Widget",
"price": 9.99,
"createdAt": "2024-01-15",
"internalCode": "WGT-001"
}
""".trimIndent()
val product = json.decodeFromString<Product>(apiResponse)
println(product) // Product(id=42, name=Widget, price=9.99)
// Parse a JSON array
val arrayJson = """[{"id":1,"name":"A","price":1.0},{"id":2,"name":"B","price":2.0}]"""
val products: List<Product> = json.decodeFromString(arrayJson)
println(products.size) // 2| Config option | Default | Effect |
|---|---|---|
ignoreUnknownKeys | false | Throw on unknown fields (set to true for API responses) |
isLenient | false | Allow unquoted strings and other relaxed JSON |
encodeDefaults | false | Include fields with default values in output |
prettyPrint | false | Human-readable indented output |
coerceInputValues | false | Coerce nulls to default values for non-nullable types |
Gson: parsing with Google's library
Gson is Google's Java JSON library that works well in Kotlin via Java interop. It uses reflection at runtime — no annotations or code generation are required for basic use. Its main drawback is that it doesn't understand Kotlin's null safety: a non-nullable String field can end up as null at runtime if the JSON contains null, bypassing the Kotlin type system. Gson is a solid choice for existing Android projects with established Retrofit setups.
// build.gradle.kts
dependencies {
implementation("com.google.code.gson:gson:2.11.0")
}import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
data class User(val id: Int, val name: String, val email: String?)
fun main() {
val gson = Gson()
// ── Decode (parse) JSON → data class ────────────────────────────────────
val jsonStr = """{"id": 1, "name": "Alice", "email": "alice@example.com"}"""
val user: User = gson.fromJson(jsonStr, User::class.java)
println(user.name) // Alice
// ── Encode (serialize) data class → JSON ────────────────────────────────
val encoded = gson.toJson(user)
println(encoded)
// {"id":1,"name":"Alice","email":"alice@example.com"}
// ── Parse a JSON array (requires TypeToken) ──────────────────────────────
// Java erases generics at runtime, so Gson needs a TypeToken to know the element type
val arrayJson = """[{"id":1,"name":"Alice","email":null},{"id":2,"name":"Bob","email":null}]"""
val listType = object : TypeToken<List<User>>() {}.type
val users: List<User> = gson.fromJson(arrayJson, listType)
println(users.size) // 2
// ── GsonBuilder for custom configuration ─────────────────────────────────
val prettyGson = GsonBuilder()
.setPrettyPrinting() // human-readable output
.serializeNulls() // include null fields in output
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss") // custom date format
.create()
println(prettyGson.toJson(user))
}For Gson, the TypeToken pattern is the required boilerplate for any generic collection type. You write it once per type. With kotlinx.serialization, this is unnecessary because Kotlin's reified generics carry type information at the call site. See the parse JSON in Java guide for more Gson patterns shared with the Java ecosystem.
Moshi: Kotlin-idiomatic JSON
Moshi is Square's JSON library designed to be Kotlin-idiomatic. It supports both reflection (via KotlinJsonAdapterFactory) and compile-time code generation (via KSP with @JsonClass(generateAdapter = true)). Unlike Gson, Moshi respects Kotlin's null safety — it throws a JsonDataException if a non-nullable field receives null. Moshi is JVM/Android only (no Kotlin Multiplatform support).
// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "2.0.0-1.0.21" // for code generation (optional)
}
dependencies {
implementation("com.squareup.moshi:moshi:1.15.1")
implementation("com.squareup.moshi:moshi-kotlin:1.15.1") // KotlinJsonAdapterFactory
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.1") // KSP code gen (optional)
}import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
// @JsonClass(generateAdapter = true) triggers KSP code generation (preferred for production)
// Without it, Moshi falls back to reflection via KotlinJsonAdapterFactory
@JsonClass(generateAdapter = true)
data class User(val id: Int, val name: String, val email: String?)
fun main() {
// Build Moshi instance — KotlinJsonAdapterFactory handles Kotlin data classes via reflection
val moshi = Moshi.Builder()
.addLast(KotlinJsonAdapterFactory())
.build()
val adapter = moshi.adapter(User::class.java)
// ── Decode (parse) JSON → data class ────────────────────────────────────
val jsonStr = """{"id": 1, "name": "Alice", "email": "alice@example.com"}"""
val user: User? = adapter.fromJson(jsonStr)
println(user?.name) // Alice
// ── Encode (serialize) data class → JSON ────────────────────────────────
val encoded = adapter.toJson(user)
println(encoded)
// {"id":1,"name":"Alice","email":"alice@example.com"}
// ── Parse a JSON array ───────────────────────────────────────────────────
val listAdapter = moshi.adapter<List<User>>(
com.squareup.moshi.Types.newParameterizedType(List::class.java, User::class.java)
)
val arrayJson = """[{"id":1,"name":"Alice","email":null}]"""
val users: List<User>? = listAdapter.fromJson(arrayJson)
println(users?.size) // 1
}| Feature | Gson | Moshi | kotlinx.serialization |
|---|---|---|---|
| Kotlin null safety | No (bypasses it) | Yes | Yes |
| Reflection-free option | No | Yes (KSP) | Yes (always) |
| Kotlin Multiplatform | No | No | Yes |
| Annotations required | No | Optional | Yes (@Serializable) |
| Retrofit converter | GsonConverterFactory | MoshiConverterFactory | asConverterFactory() |
Handling nulls, errors, and nested objects
Kotlin's null safety system maps cleanly to JSON's optional and null values when using kotlinx.serialization. Understanding this mapping is key to writing robust parsing code. Always validate your JSON with the JSON formatter before debugging deserialization failures.
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
import kotlinx.serialization.json.Json
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.SerializationException
// ── Null safety ──────────────────────────────────────────────────────────────
@Serializable
data class UserProfile(
val id: Int,
val name: String,
val email: String?, // nullable: JSON null or absent field → Kotlin null
val bio: String? = null, // nullable with default: absent field also gives null
val age: Int = 0, // non-nullable with default: absent field gives 0
)
// ── @SerialName: map different JSON key names to Kotlin fields ───────────────
@Serializable
data class ApiUser(
@SerialName("user_id") val id: Int, // JSON "user_id" → Kotlin id
@SerialName("full_name") val name: String, // JSON "full_name" → Kotlin name
@SerialName("created_at") val createdAt: String,
)
// ── Nested objects: annotate nested classes too ──────────────────────────────
@Serializable
data class Address(val city: String, val country: String)
@Serializable
data class Person(val name: String, val address: Address)
// ── Error handling ───────────────────────────────────────────────────────────
val lenientJson = Json { ignoreUnknownKeys = true }
fun parseUser(jsonStr: String): UserProfile? {
return try {
lenientJson.decodeFromString<UserProfile>(jsonStr)
} catch (e: SerializationException) {
// Malformed JSON or type mismatch (e.g. string where int expected)
println("Serialization error: " + e.message)
null
} catch (e: IllegalArgumentException) {
// Unexpected structural issue
println("Invalid argument: " + e.message)
null
}
}
fun main() {
// Nested object parsing
val personJson = """{"name":"Alice","address":{"city":"Berlin","country":"DE"}}"""
val person = lenientJson.decodeFromString<Person>(personJson)
println(person.address.city) // Berlin
// @SerialName example
val apiJson = """{"user_id":1,"full_name":"Bob","created_at":"2024-01-01"}"""
val apiUser = lenientJson.decodeFromString<ApiUser>(apiJson)
println(apiUser.name) // Bob
// Error handling
val bad = parseUser("""{"id": "not-a-number", "name": "Alice"}""")
// Serialization error: Expected int, but found string at path: $.id
}Parsing JSON on Android with Retrofit
Retrofit is the standard HTTP client for Android. It uses converter factories to automatically parse JSON response bodies into your data classes. Each JSON library has a corresponding Retrofit converter. Never parse JSON on the main thread — it blocks the UI and causes ANR (Application Not Responding) errors. Use Kotlin coroutines with suspend functions; Retrofit handles thread dispatching automatically when you use its coroutine adapter.
Retrofit + kotlinx.serialization (recommended)
// build.gradle.kts
dependencies {
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
}
// ── Setup ────────────────────────────────────────────────────────────────────
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.http.GET
import retrofit2.http.Path
val networkJson = Json { ignoreUnknownKeys = true }
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(networkJson.asConverterFactory("application/json".toMediaType()))
.build()
// ── Define API interface ─────────────────────────────────────────────────────
interface UserApi {
@GET("users/{id}")
suspend fun getUser(@Path("id") id: Int): User // suspend = coroutine-friendly
}
// ── Use in a ViewModel (never parse on main thread!) ────────────────────────
class UserViewModel : ViewModel() {
private val api = retrofit.create(UserApi::class.java)
fun loadUser(id: Int) {
viewModelScope.launch {
// Retrofit automatically dispatches IO work off the main thread
val user = api.getUser(id)
// Update UI state here — already back on main thread
}
}
}Retrofit + Gson and Moshi converters
// Retrofit + Gson
import retrofit2.converter.gson.GsonConverterFactory
val retrofitGson = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
// Retrofit + Moshi
import retrofit2.converter.moshi.MoshiConverterFactory
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
val retrofitMoshi = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()| Library | Coroutine support | Multiplatform | Reflection-free | Code gen required |
|---|---|---|---|---|
| kotlinx.serialization | Yes | Yes | Yes | Yes (@Serializable) |
| Gson | Yes (via Retrofit) | No | No | No |
| Moshi | Yes (via Retrofit) | No | Optional (KSP) | Optional |
For background on JSON parsing patterns shared with Android's Java roots, see the parse JSON in Java guide. For typed schema validation before parsing, see the JSON Schema validation guide.
Frequently asked questions
Which JSON library should I use in Kotlin?
For new Kotlin projects, use kotlinx.serialization — it's the official JetBrains library, supports all Kotlin targets (JVM, JS, Native, Wasm), generates serializers at compile time (no runtime reflection), and handles Kotlin null safety correctly. For existing Android projects that already use Retrofit with Gson, switching has limited benefit unless you need Kotlin Multiplatform. Moshi is a good middle ground — Kotlin-idiomatic with optional code generation, but limited to JVM/Android. Choose kotlinx.serialization for new projects, Gson/Moshi for Android projects with existing Retrofit setups.
How do I parse a JSON array in Kotlin with kotlinx.serialization?
Use Json.decodeFromString<List<User>>(jsonStr). The generic type parameter is reified, so no TypeToken or explicit class token is needed. For custom configuration (like ignoring unknown keys), create a Json instance first: val json = Json { ignoreUnknownKeys = true }; val users = json.decodeFromString<List<User>>(jsonStr). The returned List<User> is a standard Kotlin immutable list.
How do I handle unknown JSON fields in kotlinx.serialization?
By default, kotlinx.serialization throws SerializationException: Encountered an unknown key 'extraField' when the JSON contains fields not in your data class. This is intentional — strict deserialization catches API changes early. To allow extra fields (common for real-world APIs), configure: val json = Json { ignoreUnknownKeys = true }. You can also annotate a field with @JsonNames to map multiple JSON key names to one Kotlin field.
How do I serialize a data class to JSON in Kotlin?
With kotlinx.serialization, annotate the class with @Serializable and call Json.encodeToString(user). Fields with default values are excluded unless encodeDefaults = true. For pretty-printed output: val json = Json { prettyPrint = true }; json.encodeToString(user). With Gson: Gson().toJson(user) — no annotations needed. With Moshi: moshi.adapter(User::class.java).toJson(user).
What is the difference between Gson TypeToken and kotlinx.serialization for lists?
Gson requires a TypeToken workaround for generic types because Java erases generics at runtime: val type = object: TypeToken<List<User>>() {}.type. kotlinx.serialization uses Kotlin's reified generics, so Json.decodeFromString<List<User>>(str) works directly with no token. The TypeToken pattern is boilerplate you write once per collection type with Gson; with kotlinx.serialization, the generic parameter is inferred at the call site. For more on this pattern, see the parse JSON in C# and parse JSON in Go guides for language comparisons.
How do I handle null JSON values in Kotlin?
kotlinx.serialization maps JSON null to Kotlin nullable types. If your data class has val email: String?, a JSON "email": null or a missing "email" field (with a default of null) will deserialize correctly to Kotlin null. If the field is non-nullable (val email: String) and the JSON contains null, kotlinx.serialization throws a SerializationException. To handle this gracefully, either make the field nullable or set coerceInputValues = true in your Json configuration to substitute the default value for null.
Validate your JSON
Paste your JSON into Jsonic to catch syntax errors before parsing in Kotlin.
Open JSON Formatter