cyberangles guide

Working with JSON in Kotlin: A Step-by-Step Guide

JSON (JavaScript Object Notation) has become the de facto standard for data exchange in modern applications, powering everything from API responses and configuration files to mobile app backends. Its simplicity, readability, and lightweight nature make it ideal for transmitting structured data. Kotlin, with its concise syntax, null safety, and seamless interoperability with Java, is a perfect language for working with JSON. Whether you’re building an Android app, a backend service, or a desktop tool, mastering JSON handling in Kotlin is essential. This guide will walk you through everything you need to know about working with JSON in Kotlin, from manual parsing to using popular libraries like Gson, Moshi, and Jackson. We’ll cover serialization (converting Kotlin objects to JSON), deserialization (converting JSON to Kotlin objects), handling complex data structures, and best practices to avoid common pitfalls. By the end, you’ll be equipped to efficiently integrate JSON into your Kotlin projects.

Table of Contents

  1. Understanding JSON and Kotlin
    • 1.1 What is JSON?
    • 1.2 Kotlin Data Structures and JSON Mapping
  2. Manual JSON Parsing with Kotlin
    • 2.1 Setup: Using the org.json Library
    • 2.2 Parsing JSON Strings to Kotlin Objects
    • 2.3 Creating JSON Strings from Kotlin Objects
  3. Using Gson for JSON Handling
    • 3.1 Setup: Adding Gson to Your Project
    • 3.2 Serialization: Kotlin Object → JSON String
    • 3.3 Deserialization: JSON String → Kotlin Object
    • 3.4 Handling Complex Cases (Nested Objects, Lists, Custom Types)
  4. Using Moshi for Modern JSON Handling
    • 4.1 Setup: Adding Moshi to Your Project
    • 4.2 Serialization and Deserialization with Moshi
    • 4.3 Kotlin-Specific Features (Default Values, Sealed Classes)
  5. Using Jackson for Enterprise-Grade JSON Handling
    • 5.1 Setup: Adding Jackson to Your Project
    • 5.2 Serialization and Deserialization with Jackson
    • 5.3 Kotlin Compatibility with Jackson Modules
  6. Comparing JSON Libraries for Kotlin
  7. Best Practices for JSON Handling in Kotlin
  8. Conclusion
  9. References

1. Understanding JSON and Kotlin

1.1 What is JSON?

JSON is a text-based format for storing and transmitting data. It supports two primary structures:

  • Objects: Key-value pairs enclosed in {}, e.g., {"name": "Alice", "age": 30}.
  • Arrays: Ordered lists of values enclosed in [], e.g., ["apple", "banana", "cherry"].

Values can be strings, numbers, booleans, null, objects, or arrays, making JSON flexible for complex data.

1.2 Kotlin Data Structures and JSON Mapping

Kotlin’s type system aligns naturally with JSON. For example:

  • JSON objects map to Kotlin data classes (or classes with properties).
  • JSON arrays map to Kotlin List or Array.
  • JSON primitives (strings, numbers) map to Kotlin primitives (String, Int, Boolean, etc.).

Consider this sample JSON:

{
  "user": {
    "name": "Alice",
    "age": 30,
    "hobbies": ["reading", "hiking"],
    "isStudent": false
  }
}

In Kotlin, this could be represented with nested data classes:

data class User(
  val name: String,
  val age: Int,
  val hobbies: List<String>,
  val isStudent: Boolean
)

data class UserResponse(val user: User)

This alignment simplifies converting between JSON and Kotlin objects.

2. Manual JSON Parsing with Kotlin

While manual parsing is error-prone and not recommended for production, understanding the basics helps you appreciate the value of libraries. We’ll use the lightweight org.json library (JSON-java) for manual operations.

2.1 Setup

Add the org.json dependency to your project. For Gradle (JVM/Android):

dependencies {
  implementation 'org.json:json:20230227' // Check for latest version
}

2.2 Parsing JSON

To parse a JSON string into a Kotlin object, use JSONObject and JSONArray:

Example: Parsing a JSON String

import org.json.JSONObject

fun main() {
  val jsonString = """
    {
      "name": "Alice",
      "age": 30,
      "hobbies": ["reading", "hiking"],
      "isStudent": false
    }
  """.trimIndent()

  // Parse JSON string into a JSONObject
  val jsonObject = JSONObject(jsonString)

  // Extract values (handle exceptions for missing keys!)
  val name = jsonObject.getString("name") // "Alice"
  val age = jsonObject.getInt("age") // 30
  val isStudent = jsonObject.getBoolean("isStudent") // false

  // Extract array
  val hobbiesArray = jsonObject.getJSONArray("hobbies")
  val hobbies = mutableListOf<String>()
  for (i in 0 until hobbiesArray.length()) {
    hobbies.add(hobbiesArray.getString(i))
  }

  // Create a User object
  val user = User(name, age, hobbies, isStudent)
  println(user) // User(name=Alice, age=30, hobbies=[reading, hiking], isStudent=false)
}

Note: org.json throws JSONException if a key is missing or the type is incorrect (e.g., calling getInt("name")). Always validate data or use optXxx() methods (e.g., optString("name", "Unknown")) for safe defaults.

2.3 Creating JSON Manually

You can build JSON strings by constructing JSONObject and JSONArray instances:

import org.json.JSONArray
import org.json.JSONObject

fun main() {
  // Build a JSON object
  val userJson = JSONObject().apply {
    put("name", "Bob")
    put("age", 25)
    put("hobbies", JSONArray(listOf("gaming", "coding")))
    put("isStudent", true)
  }

  // Convert to string
  val jsonString = userJson.toString(2) // Pretty-printed with 2-space indent
  println(jsonString)
}

Output:

{
  "name": "Bob",
  "age": 25,
  "hobbies": ["gaming", "coding"],
  "isStudent": true
}

Manual parsing is tedious for large/complex JSON. For real-world apps, use libraries like Gson, Moshi, or Jackson.

3. Using Gson for JSON Handling

Gson, developed by Google, is a popular library for serializing (Kotlin object → JSON) and deserializing (JSON → Kotlin object) with minimal code. It uses reflection to map objects to JSON, making it easy to use.

3.1 Setup

Add Gson to your project. For Gradle (Android/JVM):

dependencies {
  implementation 'com.google.code.gson:gson:2.10.1' // Check for latest version
}

3.2 Serialization (Kotlin Object → JSON)

Serialize a Kotlin object to a JSON string with Gson.toJson().

Example: Serialize a Data Class

import com.google.gson.Gson

data class User(
  val name: String,
  val age: Int,
  val hobbies: List<String>,
  val isStudent: Boolean
)

fun main() {
  val user = User(
    name = "Charlie",
    age = 28,
    hobbies = listOf("photography", "cooking"),
    isStudent = false
  )

  val gson = Gson()
  val jsonString = gson.toJson(user) // Convert object to JSON
  println(jsonString)
}

Output:

{"name":"Charlie","age":28,"hobbies":["photography","cooking"],"isStudent":false}

3.3 Deserialization (JSON → Kotlin Object)

Deserialize a JSON string to a Kotlin object with Gson.fromJson().

Example: Deserialize JSON to Data Class

fun main() {
  val jsonString = """
    {"name":"Diana","age":32,"hobbies":["yoga","painting"],"isStudent":true}
  """.trimIndent()

  val gson = Gson()
  val user = gson.fromJson(jsonString, User::class.java) // JSON → User object
  println(user.name) // "Diana"
  println(user.hobbies) // ["yoga", "painting"]
}

3.4 Handling Complex Cases

Nested Objects

Gson automatically handles nested data classes:

data class Address(val street: String, val city: String)
data class UserWithAddress(val user: User, val address: Address)

fun main() {
  val userWithAddress = UserWithAddress(
    user = User("Eve", 35, listOf("traveling"), false),
    address = Address("123 Main St", "New York")
  )

  val json = Gson().toJson(userWithAddress)
  println(json)
}

Output:

{
  "user": {"name":"Eve","age":35,"hobbies":["traveling"],"isStudent":false},
  "address": {"street":"123 Main St","city":"New York"}
}

Custom Serialization/Deserialization

For non-trivial cases (e.g., formatting dates), use custom TypeAdapter or JsonSerializer/JsonDeserializer.

Example: Custom Date Format
Suppose you have a User with a birthDate field (e.g., LocalDate). Gson doesn’t natively support LocalDate, so define a custom deserializer:

import com.google.gson.*
import java.time.LocalDate
import java.time.format.DateTimeFormatter

class LocalDateDeserializer : JsonDeserializer<LocalDate> {
  override fun deserialize(
    json: JsonElement?,
    typeOfT: java.lang.reflect.Type?,
    context: JsonDeserializationContext?
  ): LocalDate {
    return LocalDate.parse(json?.asString, DateTimeFormatter.ISO_LOCAL_DATE)
  }
}

// Update User to include birthDate
data class User(
  val name: String,
  val age: Int,
  val birthDate: LocalDate // New field
)

fun main() {
  val jsonString = """{"name":"Frank","age":33,"birthDate":"1990-05-15"}"""
  
  val gson = GsonBuilder()
    .registerTypeAdapter(LocalDate::class.java, LocalDateDeserializer())
    .create()

  val user = gson.fromJson(jsonString, User::class.java)
  println(user.birthDate) // 1990-05-15
}

4. Using Moshi for Modern JSON Handling

Moshi, developed by Square, is a modern alternative to Gson with better Kotlin support. It uses code generation (via kapt) or reflection and handles Kotlin-specific features like null safety, default values, and sealed classes.

4.1 Setup

Add Moshi to your project. For Gradle (Kotlin with code generation):

dependencies {
  implementation 'com.squareup.moshi:moshi:1.15.0' // Core library
  kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.15.0' // Code generation (optional but recommended)
}

Enable kapt in your build.gradle:

plugins {
  kotlin("kapt") version "1.9.0" // Match your Kotlin version
}

4.2 Serialization and Deserialization

Moshi uses JsonAdapter to convert between objects and JSON. With code generation, adapters are generated at compile time for better performance.

Example: Basic Serialization/Deserialization

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory

data class User(
  val name: String,
  val age: Int,
  val hobbies: List<String>,
  val isStudent: Boolean
)

fun main() {
  // Build Moshi instance with Kotlin support
  val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory()) // Required for Kotlin data classes
    .build()

  // Create an adapter for the User class
  val userAdapter = moshi.adapter(User::class.java)

  // Serialize: User → JSON
  val user = User("Grace", 29, listOf("dancing", "singing"), false)
  val jsonString = userAdapter.toJson(user)
  println(jsonString) // {"name":"Grace","age":29,"hobbies":["dancing","singing"],"isStudent":false}

  // Deserialize: JSON → User
  val deserializedUser = userAdapter.fromJson(jsonString)
  println(deserializedUser?.name) // "Grace"
}

4.3 Kotlin-Specific Features

Moshi excels at handling Kotlin features like:

Default Values

Moshi preserves default values in data classes, unlike Gson (which requires workarounds):

data class User(
  val name: String,
  val age: Int,
  val hobbies: List<String> = emptyList(), // Default value
  val isStudent: Boolean = false // Default value
)

fun main() {
  val jsonString = """{"name":"Heidi","age":26}""" // Missing hobbies and isStudent
  val user = moshi.adapter(User::class.java).fromJson(jsonString)
  println(user?.hobbies) // [] (uses default)
  println(user?.isStudent) // false (uses default)
}

Sealed Classes and Enums

Moshi natively supports sealed classes and enums, critical for polymorphic data:

sealed class PaymentMethod {
  data class CreditCard(val number: String, val expiry: String) : PaymentMethod()
  data class PayPal(val email: String) : PaymentMethod()
}

data class Order(val id: String, val paymentMethod: PaymentMethod)

fun main() {
  val order = Order(
    id = "ORD123",
    paymentMethod = PaymentMethod.CreditCard("4111-1111-1111-1111", "12/25")
  )

  val json = moshi.adapter(Order::class.java).toJson(order)
  println(json) 
  // {"id":"ORD123","paymentMethod":{"type":"CreditCard","number":"4111-1111-1111-1111","expiry":"12/25"}}
}

5. Using Jackson for Enterprise-Grade JSON Handling

Jackson is a powerful, widely used library in the JVM ecosystem, known for its performance and extensive feature set. It supports JSON, XML, and other formats, making it ideal for enterprise applications. For Kotlin, use jackson-module-kotlin to handle data classes, null safety, and other Kotlin features.

5.1 Setup

Add Jackson dependencies to your project:

dependencies {
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' // Core
  implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2' // Kotlin support
}

5.2 Serialization and Deserialization

Jackson uses ObjectMapper to convert between objects and JSON.

Example: Basic Usage

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue

data class User(
  val name: String,
  val age: Int,
  val hobbies: List<String>
)

fun main() {
  // Create ObjectMapper with Kotlin support
  val objectMapper = jacksonObjectMapper()

  // Serialize: User → JSON
  val user = User("Ivy", 31, listOf("cycling", "gardening"))
  val jsonString = objectMapper.writeValueAsString(user)
  println(jsonString) // {"name":"Ivy","age":31,"hobbies":["cycling","gardening"]}

  // Deserialize: JSON → User (using readValue extension from jackson-module-kotlin)
  val deserializedUser: User = objectMapper.readValue(jsonString)
  println(deserializedUser.age) // 31
}

5.3 Handling Annotations

Jackson supports annotations for customizing behavior (e.g., renaming fields):

import com.fasterxml.jackson.annotation.JsonProperty

data class User(
  @JsonProperty("full_name") // Rename JSON key to "full_name"
  val name: String,
  val age: Int
)

fun main() {
  val jsonString = """{"full_name":"Jack","age":27}"""
  val user = objectMapper.readValue<User>(jsonString)
  println(user.name) // "Jack" (mapped from "full_name")
}

6. Comparing JSON Libraries for Kotlin

FeatureGsonMoshiJackson
Kotlin SupportBasic (reflection-based)Excellent (codegen/reflection)Good (via jackson-module-kotlin)
PerformanceFast (reflection)Very fast (codegen)Fast (reflection/codegen)
Ease of UseSimple (minimal setup)Simple (Kotlin-first)Slightly complex (more config)
Default ValuesNo (requires workarounds)YesYes
Null SafetyBasicStrong (Kotlin-native)Good
PolymorphismRequires custom adaptersNative (sealed classes)Requires annotations
Enterprise FeaturesLimitedLimitedExtensive (XML, CSV, validation)

Recommendation:

  • Use Moshi for Android/Kotlin-first projects (best Kotlin support).
  • Use Gson for simple use cases (minimal setup).
  • Use Jackson for enterprise backends (extensive features, ecosystem).

7. Best Practices for JSON Handling in Kotlin

  1. Use Data Classes: They are immutable, concise, and work seamlessly with libraries.
  2. Handle Nulls Explicitly: Use ? for nullable fields and validate JSON input.
  3. Avoid Manual Parsing: Use libraries to reduce boilerplate and errors.
  4. Validate JSON: Use tools like JSON Schema to ensure data integrity.
  5. Leverage Code Generation: For Moshi/Jackson, use code generation (e.g., kapt) for faster runtime performance.
  6. Test Serialization/Deserialization: Write unit tests to catch edge cases (e.g., missing fields).

8. Conclusion

Working with JSON in Kotlin is straightforward with libraries like Gson, Moshi, and Jackson. Manual parsing is useful for learning but impractical for production. Moshi stands out for Kotlin-first projects, while Gson and Jackson excel in simplicity and enterprise features, respectively. By following best practices like using data classes and validating input, you can efficiently handle JSON in any Kotlin application.

9. References