Question

How to Parse JSON in Kotlin: Strings, Objects, and Mapping to Classes

kotlinjavajson

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 name and age, 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.

Take Quiz

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

  • @Serializable tells Kotlin how to encode and decode the class.
  • Json.decodeFromString<User>(jsonString) parses the JSON string.
  • The nested address object is automatically mapped to the Address class.

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

  1. The program defines two data classes:

    • Profile
    • Account
  2. Account contains a property named profile of type Profile.

  3. The JSON string contains:

    • id as a number
    • profile as a nested object
  4. Json.decodeFromString<Account>(jsonString) starts reading the JSON.

  5. It sees id: 10 and stores that in the id property.

  6. It sees the nested profile object and notices that Account.profile expects a Profile.

  7. It reads username from the nested object and creates Profile(username = "kotlin_dev").

  8. It creates the final object:

Account(
    id = 10,
    profile = Profile(username = "kotlin_dev")
)
  1. 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.

Take Quiz

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

ApproachBest forProsCons
Raw JSON parsingInspecting a few fields or dynamic JSONFlexible, no need for many classesMore manual work, less type safety
Mapping to data classesKnown API responsesClean, type-safe, easier to maintainRequires matching class definitions
Custom deserializerSpecial or inconsistent formatsHandles unusual data shapesMore code, harder to maintain

kotlinx.serialization vs Jackson

LibraryStrengthsConsiderations
kotlinx.serializationKotlin-first, concise, good for data classesRequires setup and @Serializable annotations
JacksonVery common, powerful, widely used in JVM appsCan be more configuration-heavy

Manual parsing vs automatic mapping

TechniqueWhen to use
parseToJsonElementWhen you only need to inspect or extract parts of JSON
decodeFromString<T> or readValue<T>()When you want fully typed Kotlin objects
Take Quiz

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.

Take Quiz

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

  1. Create Kotlin data classes for a blog post, author, and metadata.
  2. Use a JSON string that contains nested objects.
  3. Parse the JSON string into Kotlin objects.
  4. Print the post title, author name, and view count.
  5. Keep the solution focused on parsing and mapping, not networking.
Take Quiz

Keep learning

Related questions

View more