Table of Contents
- Understanding JSON and Kotlin
- 1.1 What is JSON?
- 1.2 Kotlin Data Structures and JSON Mapping
- Manual JSON Parsing with Kotlin
- 2.1 Setup: Using the
org.jsonLibrary - 2.2 Parsing JSON Strings to Kotlin Objects
- 2.3 Creating JSON Strings from Kotlin Objects
- 2.1 Setup: Using the
- 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)
- 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)
- 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
- Comparing JSON Libraries for Kotlin
- Best Practices for JSON Handling in Kotlin
- Conclusion
- 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(orclasses with properties). - JSON arrays map to Kotlin
ListorArray. - 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
| Feature | Gson | Moshi | Jackson |
|---|---|---|---|
| Kotlin Support | Basic (reflection-based) | Excellent (codegen/reflection) | Good (via jackson-module-kotlin) |
| Performance | Fast (reflection) | Very fast (codegen) | Fast (reflection/codegen) |
| Ease of Use | Simple (minimal setup) | Simple (Kotlin-first) | Slightly complex (more config) |
| Default Values | No (requires workarounds) | Yes | Yes |
| Null Safety | Basic | Strong (Kotlin-native) | Good |
| Polymorphism | Requires custom adapters | Native (sealed classes) | Requires annotations |
| Enterprise Features | Limited | Limited | Extensive (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
- Use Data Classes: They are immutable, concise, and work seamlessly with libraries.
- Handle Nulls Explicitly: Use
?for nullable fields and validate JSON input. - Avoid Manual Parsing: Use libraries to reduce boilerplate and errors.
- Validate JSON: Use tools like JSON Schema to ensure data integrity.
- Leverage Code Generation: For Moshi/Jackson, use code generation (e.g.,
kapt) for faster runtime performance. - 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.