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.

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 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:
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.
A very common pattern is unpacking pairs and query results:
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.
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:
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.
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.
The Kotlin standard library provides Result<T> to represent a success or failure without throwing an exception:
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.
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.
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:
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.
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.
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:
val (a, b) = objek unpacks a data class based on property order.runCatching and Result handle failures without scattered try-catch blocks.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.