Question
I am receiving a deeply nested JSON string from a service, and I need to parse it into a JSON object and then map it to Kotlin classes.
How can I convert a JSON string into an object in Kotlin?
After that, I need to map the parsed data to the appropriate classes. I was previously using StdDeserializer from Jackson, but I ran into problems when some properties also needed to be deserialized into custom classes. I did not know how to access or reuse the object mapper inside another deserializer.
Preferably, I would like a solution with minimal dependencies. If the answer focuses only on JSON parsing and manipulation, that would still be enough.
Short Answer
By the end of this page, you will understand how JSON parsing works in Kotlin, how to turn a JSON string into structured objects, and how to map nested JSON into Kotlin data classes. You will also see when to use manual parsing versus automatic mapping, and how common libraries such as Jackson or kotlinx.serialization help with nested objects.
Concept
JSON parsing in Kotlin usually means one of two things:
- Reading raw JSON data so you can inspect keys and values
- Mapping JSON directly into Kotlin classes so you can work with typed objects
A JSON string is just text until a parser reads it and turns it into a structure your program understands.
For example, this JSON:
{
"name": "Ada",
"age": 31
}
can be treated as:
- a generic JSON object with keys like
nameandage, or - a Kotlin class such as:
data class User(val name: String, val age: Int)
In real Kotlin projects, developers usually prefer mapping JSON straight into data classes because:
- the code becomes safer and easier to read
- nested JSON can become nested Kotlin objects
- the compiler helps catch mistakes
- you avoid lots of manual key lookups
If your JSON is deeply nested, manual parsing becomes tedious very quickly. That is why JSON libraries are commonly used.
Common approaches in Kotlin
1. Manual parsing into JSON objects
Useful when:
- you only need a few fields
- the structure is dynamic or unknown
- you want minimal mapping logic
2. Automatic mapping into data classes
Useful when:
- the JSON structure is known
- you want strongly typed Kotlin objects
- the JSON contains nested objects and lists
About “native” Kotlin
Kotlin does not include a full built-in JSON parser in the standard library. In practice, you usually use a library such as:
kotlinx.serialization- Jackson
- Gson
- Moshi
If you want fewer dependencies and a Kotlin-first approach, kotlinx.serialization is often the most natural option. If you are already using Jackson, it can also handle nested objects well without requiring custom deserializers in many cases.
Mental Model
Think of a JSON string as a packed delivery box with labels.
- Parsing JSON means opening the box and reading what is inside.
- Mapping JSON to Kotlin classes means placing each item into the correct shelf or container.
If the box contains another box inside it, that is like a nested JSON object. Your Kotlin class can also contain another class to match that structure.
For example:
- outer JSON object → outer Kotlin data class
- nested JSON object → nested Kotlin data class
- JSON array → Kotlin
List
If your shelves are labeled correctly, the library can often put everything in the right place automatically.
Syntax and Examples
Parsing and mapping with kotlinx.serialization
A common Kotlin-first solution is kotlinx.serialization.
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class Address(
val city: String,
val zip: String
)
@Serializable
data class User(
val name: String,
val age: Int,
val address: Address
)
fun main() {
val jsonString = """
{
"name": "Ada",
"age": 31,
"address": {
"city": "London",
"zip": "SW1"
}
}
""".trimIndent()
val user = Json.decodeFromString<User>(jsonString)
println(user)
println(user.address.city)
}
What this does
@Serializabletells Kotlin how to encode and decode the class.Json.decodeFromString<User>(jsonString)parses the JSON string.- The nested
addressobject is automatically mapped to theAddressclass.
Parsing raw JSON without mapping everything
If you only want to inspect the JSON structure:
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
fun main() {
val jsonString = """
{ "name": "Ada", "age": 31 }
""".trimIndent()
val element = Json.parseToJsonElement(jsonString)
val obj = element.jsonObject
val name = obj["name"]?.jsonPrimitive?.content
val age = obj["age"]?.jsonPrimitive?.content
println(name)
println(age)
}
This approach is useful when you need basic JSON manipulation but do not want full class mapping.
Mapping with Jackson
If you are already using Jackson, nested objects usually map automatically if your classes match the JSON structure.
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
data class Address(
val city: String,
val zip: String
)
data class User(
val name: String,
val age: Int,
val address: Address
)
fun main() {
val jsonString = """
{
"name": "Ada",
"age": 31,
"address": {
"city": "London",
"zip": "SW1"
}
}
""".trimIndent()
val mapper = jacksonObjectMapper()
val user: User = mapper.readValue(jsonString)
println(user)
}
In many cases, you do not need a custom StdDeserializer for nested objects if the JSON shape already matches your data classes.
Step by Step Execution
Consider this Kotlin code:
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class Profile(val username: String)
@Serializable
data class Account(val id: Int, val profile: Profile)
fun main() {
val jsonString = """
{
"id": 10,
"profile": {
"username": "kotlin_dev"
}
}
""".trimIndent()
val account = Json.decodeFromString<Account>(jsonString)
println(account)
}
Step by step
-
The program defines two data classes:
ProfileAccount
-
Accountcontains a property namedprofileof typeProfile. -
The JSON string contains:
idas a numberprofileas a nested object
-
Json.decodeFromString<Account>(jsonString)starts reading the JSON. -
It sees
id: 10and stores that in theidproperty. -
It sees the nested
profileobject and notices thatAccount.profileexpects aProfile. -
It reads
usernamefrom the nested object and createsProfile(username = "kotlin_dev"). -
It creates the final object:
Account(
id = 10,
profile = Profile(username = "kotlin_dev")
)
println(account)prints the created Kotlin object.
This is the key idea: nested JSON becomes nested Kotlin objects when the class structure matches the JSON structure.
Real World Use Cases
JSON parsing in Kotlin is used in many common situations:
API responses
A mobile app or backend service receives JSON from a REST API and maps it to data classes.
data class Product(val id: Int, val name: String, val price: Double)
Configuration files
An application reads JSON configuration at startup.
Examples:
- feature flags
- API endpoints
- environment settings
Caching remote data
A service fetches JSON once, saves it, and loads it later into classes.
Event processing
A queue or webhook sends JSON payloads that must be validated and transformed into domain objects.
Partial JSON inspection
Sometimes you only need one field from a large payload, such as:
- status
- error message
- token
In that case, raw JSON parsing can be enough without mapping the full structure.
Real Codebase Usage
In real projects, developers usually choose one of these patterns:
1. Map directly to data classes
This is the most common pattern when the JSON schema is known.
@Serializable
data class ApiResponse(val success: Boolean, val data: User)
This keeps application code clean and type-safe.
2. Use nested classes to match nested JSON
If the JSON contains objects inside objects, developers define nested models.
data class Order(val id: String, val customer: Customer)
data class Customer(val name: String, val email: String)
3. Use nullable properties for optional fields
Real APIs often omit fields.
@Serializable
data class User(
val name: String,
val nickname: String? = null
)
4. Use custom deserialization only when necessary
Custom deserializers are usually reserved for cases like:
- inconsistent API formats
- date parsing
- field names that need special conversion
- values that can have multiple shapes
If the JSON structure is normal and predictable, automatic mapping is simpler.
5. Validate after parsing
Teams often parse first, then validate business rules.
Examples:
- required combinations of fields
- value ranges
- unsupported states
6. Handle unknown fields safely
Real APIs may add fields over time. Many developers configure parsers to ignore fields they do not use.
With kotlinx.serialization:
val json = Json {
ignoreUnknownKeys = true
}
This helps prevent parsing failures when APIs evolve.
Common Mistakes
1. Expecting Kotlin standard library to parse JSON by itself
Kotlin does not include a full JSON parser in the standard library.
You usually need a library such as kotlinx.serialization, Jackson, Gson, or Moshi.
2. Class structure does not match JSON structure
Broken example:
data class User(val name: String, val city: String)
But the JSON is:
{
"name": "Ada",
"address": {
"city": "London"
}
}
This will not map correctly because city is nested inside address.
Correct version:
data class Address(val city: String)
data class User(val name: String, val address: Address)
3. Forgetting @Serializable with kotlinx.serialization
Broken example:
data class User(val name: String)
If you use kotlinx.serialization, the class must be annotated:
import kotlinx.serialization.Serializable
@Serializable
data class User(val name: String)
4. Using the wrong property types
Broken example:
@Serializable
data class User(val age: String)
If the JSON contains a number:
{ "age": 31 }
then age should usually be Int, not String.
5. Writing custom deserializers too early
Many beginners create custom deserializers for nested objects when simple nested data classes would work.
Try matching the JSON shape with your Kotlin classes first.
6. Ignoring nullability
If a field may be missing or null, make it nullable or give it a default value.
@Serializable
data class User(val nickname: String? = null)
7. Not handling unknown fields
Some parsers fail when extra fields appear. Configure your parser if needed.
val json = Json {
ignoreUnknownKeys = true
}
Comparisons
| Approach | Best for | Pros | Cons |
|---|---|---|---|
| Raw JSON parsing | Inspecting a few fields or dynamic JSON | Flexible, no need for many classes | More manual work, less type safety |
| Mapping to data classes | Known API responses | Clean, type-safe, easier to maintain | Requires matching class definitions |
| Custom deserializer | Special or inconsistent formats | Handles unusual data shapes | More code, harder to maintain |
kotlinx.serialization vs Jackson
| Library | Strengths | Considerations |
|---|---|---|
kotlinx.serialization | Kotlin-first, concise, good for data classes | Requires setup and @Serializable annotations |
| Jackson | Very common, powerful, widely used in JVM apps | Can be more configuration-heavy |
Manual parsing vs automatic mapping
| Technique | When to use |
|---|---|
parseToJsonElement | When you only need to inspect or extract parts of JSON |
decodeFromString<T> or readValue<T>() | When you want fully typed Kotlin objects |
Cheat Sheet
Quick reference
Parse JSON to Kotlin object with kotlinx.serialization
@Serializable
data class User(val name: String)
val user = Json.decodeFromString<User>(jsonString)
Parse JSON as raw JSON tree
val element = Json.parseToJsonElement(jsonString)
Access a field from a JSON object
val name = element.jsonObject["name"]?.jsonPrimitive?.content
Nested object mapping
@Serializable
data class Address(val city: String)
@Serializable
data class User(val name: String, val address: Address)
Ignore unknown fields
val json = Json {
ignoreUnknownKeys = true
}
Jackson mapping
val mapper = jacksonObjectMapper()
val user: User = mapper.readValue(jsonString)
Rules to remember
- JSON string = plain text until parsed
- JSON object maps well to Kotlin
data class - Nested JSON object maps to nested Kotlin class
- JSON array maps to Kotlin
List - Optional fields should usually be nullable or have defaults
- Use custom deserializers only for unusual cases
Common edge cases
- missing fields
- extra unknown fields
- null values
- wrong number/string types
- nested objects not matching class structure
FAQ
Can Kotlin parse JSON without a library?
Not fully with the standard library alone. In practice, you use a JSON library such as kotlinx.serialization, Jackson, Gson, or Moshi.
What is the easiest way to map JSON to Kotlin classes?
For Kotlin projects, kotlinx.serialization is often the simplest and most Kotlin-friendly option. If your project already uses Jackson, it is also a strong choice.
How do I parse nested JSON in Kotlin?
Create nested data classes that match the JSON structure. The parser can then map inner JSON objects to inner Kotlin objects automatically.
Do I need a custom deserializer for nested objects?
Usually no. If your JSON structure matches your Kotlin classes, most libraries handle nested objects automatically.
How can I read only one field from a JSON string?
Parse the JSON as a raw JSON element or object, then extract the key you need instead of mapping the whole payload.
What if the API returns extra fields I do not need?
Configure the parser to ignore unknown fields, or use a library setting that skips unused properties.
Why does my JSON mapping fail in Kotlin?
Common reasons include mismatched property names, wrong data types, missing annotations, nullability issues, or a class structure that does not match the JSON.
Mini Project
Description
Build a small Kotlin program that reads a nested JSON response representing a blog post and converts it into Kotlin data classes. This demonstrates how nested JSON objects can be mapped directly without manually extracting every field.
Goal
Parse a nested JSON string into Kotlin objects and print selected values from the mapped result.
Requirements
- Create Kotlin data classes for a blog post, author, and metadata.
- Use a JSON string that contains nested objects.
- Parse the JSON string into Kotlin objects.
- Print the post title, author name, and view count.
- Keep the solution focused on parsing and mapping, not networking.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Add a Maven Repository URL in Gradle Kotlin DSL (build.gradle.kts)
Learn how to add Maven Central and a custom Maven repository URL in Gradle Kotlin DSL using build.gradle.kts syntax.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.