Learn Kotlin - Null-safe Data Handling
Episode 8 of 23

Learn Kotlin - Null-safe Data Handling

This episode masters safe data handling: data classes and destructuring declarations, sealed classes and sealed interfaces for state, the Result type with idiomatic error handling, and data serialization using kotlinx.serialization or Jackson.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

After null safety and functional programming, episode 8 combines both for safe and idiomatic data handling. You'll learn data classes with destructuring, sealed classes and sealed interfaces for modeling state, the Result type for error handling without exceptions, and data serialization to JSON.

This approach forms Kotlin's signature style in the real world: data is modeled explicitly, nulls and errors become part of the type system, and serialization works smoothly with built-in tooling.

After this episode, you'll design data models and error flows that are far easier to understand than the classic Java style.

Data Classes and Destructuring

Data Class for Data Models

data class was already introduced in episode 4. In this episode we use its capabilities to the full, especially with destructuring declarations — unpacking an object into several variables at once based on property order:

KotlinDestructuring declaration
data class Point(val x: Int, val y: Int)
 
val p = Point(3, 5)
val (x, y) = p
println("x=$x, y=$y")

val (x, y) = p assigns p.x to x and p.y to y automatically. Destructuring also works on Map.Entry, List, and other types that provide component1, component2, and so on.

Using Destructuring on Common Data

A very common pattern is unpacking pairs and query results:

KotlinDestructuring map dan list
val harga = mapOf("kopi" to 15000, "teh" to 8000)
 
for ((nama, nilai) in harga) {
    println("$nama = $nilai")
}
val (a, b) = listOf("satu", "dua")

for ((nama, nilai) in harga) iterates over all key-value pairs without accessing .key and .value manually. Destructuring makes reading data far more concise. You can test these destructuring examples with kotlinc Main.kt -include-runtime -d main.jar.

Sealed Class and Sealed Interface for State

Sealed Interface for an Open Hierarchy

Since Kotlin 1.5, sealed interfaces extend the sealed class concept: subtypes can be implemented anywhere (not just in one file), as long as they remain in the same module. This is more flexible for large state models:

KotlinSealed interface untuk UI state
sealed interface UiState {
    data object Loading : UiState
    data class Content(val items: List<String>) : UiState
    data class Error(val message: String) : UiState
}
 
fun render(state: UiState): String = when (state) {
    is UiState.Loading -> "Memuat..."
    is UiState.Content -> "Menampilkan ${state.items.size} item"
    is UiState.Error -> "Terjadi error: ${state.message}"
}

Compile-time exhaustiveness still applies: if you add a new subtype, when will fail to compile until every branch is handled. A UiState pattern like this is very common in Android and frontend apps for modeling screen states.

Why Sealed for State

Sealed classes/interfaces provide a two-way guarantee: the compiler knows all possible states, and code readers know the state is limited to a documented set. This eliminates the "unexpected state" errors that often occur with nullable flags or nested enums.

Result Type and Error Handling

Result for Operations That Can Fail

The Kotlin standard library provides Result<T> to represent a success or failure without throwing an exception:

KotlinResult dan runCatching
fun bagi(a: Int, b: Int): Result<Int> =
    runCatching { if (b == 0) throw IllegalArgumentException() else a / b }
 
val hasil = bagi(10, 0)
hasil.onSuccess { println("Hasil: $it") }
    .onFailure { println("Gagal: ${it.message}") }

runCatching catches exceptions and wraps them into a Result. onSuccess and onFailure handle the two branches without manual try-catch. This pattern makes error handling explicit within the data flow instead of scattered across try-catch blocks.

When to Use Result

Use Result for operations whose failure is a normal part of the business flow — input parsing, network calls, validation. Keep exceptions for truly exceptional conditions like programming errors. Combining Result with flatMap or fold creates a flow that is easy to follow.

Serialization with kotlinx.serialization

Configuration and Basic Usage

kotlinx.serialization is JetBrains' official serialization library. A class becomes JSON with a single line, as long as the Gradle plugin is installed and the class is annotated @Serializable:

KotlinSerialization dengan kotlinx
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
 
@Serializable
data class Pesan(val pengirim: String, val isi: String)
 
fun main() {
    val pesan = Pesan("Arman", "Halo dari Kotlin")
    val json = Json.encodeToString(pesan)
    println(json)
    val balik = Json.decodeFromString<Pesan>(json)
    println(balik.isi)
}

The resulting JSON output: {"pengirim":"Arman","isi":"Halo dari Kotlin"}. With the org.jetbrains.kotlin.plugin.serialization plugin in build.gradle.kts, the compiler generates the serializer automatically — no annotation processing or reflection needed.

Alternative: Jackson

If you're already in the Java ecosystem, Jackson also works fully with Kotlin classes via the jackson-module-kotlin module. Its strength is a rich ecosystem for JSON configuration and streaming. For pure Kotlin projects, kotlinx.serialization is lighter and handles nullable types explicitly and works well with coroutines.

Closing

Episode 8 completes safe data handling: destructuring for unpacking data classes, sealed classes and sealed interfaces for guaranteed state, Result and runCatching for error handling without exceptions, and serialization with kotlinx.serialization or Jackson.

The key takeaways:

  • Destructuring val (a, b) = objek unpacks a data class based on property order.
  • Sealed interfaces provide an exhaustive state model at the compile level.
  • runCatching and Result handle failures without scattered try-catch blocks.
  • Use Result for business errors, exceptions for exceptional conditions.
  • kotlinx.serialization serializes JSON without reflection.
  • Jackson stays relevant for codebases already based on the Java ecosystem.

In episode 9 we'll discuss coroutines and asynchronous programming — coroutine fundamentals and suspending functions, coroutine scopes and dispatchers, launch and async, structured concurrency with exception handling, and the Flow API for reactive streams.

Learn Kotlin - Null-safe Data Handling | Learn Kotlin