YAML #
YAML (YAML Ain’t Markup Language) adalah format serialisasi data yang dirancang untuk mudah dibaca manusia. Dibandingkan JSON, YAML lebih bersih untuk file konfigurasi: tidak ada kurung kurawal, tidak ada tanda kutip yang wajib, dan komentar didukung secara native. Ini menjadikannya standar de facto untuk konfigurasi aplikasi — Spring Boot, Docker Compose, Kubernetes, GitHub Actions, semuanya menggunakan YAML. Di Kotlin, ada dua library utama: SnakeYAML (library Java yang matur dan fleksibel) dan kaml (library yang mengintegrasikan YAML dengan kotlinx.serialization untuk type-safety penuh). Artikel ini membahas keduanya secara mendalam, termasuk pola manajemen konfigurasi yang baik untuk aplikasi production.
Sintaks YAML — Referensi Cepat #
Sebelum membahas library, penting memahami sintaks YAML yang akan kamu baca dan tulis:
# Komentar dimulai dengan #
# Scalar (nilai tunggal)
nama: "Budi Santoso" # string dengan kutip
umur: 25 # integer
tinggi: 170.5 # float
aktif: true # boolean
kosong: null # null (atau ~)
tanggal: 2024-08-17 # tanggal (ISO 8601)
# String multiline
deskripsi: |
Baris pertama
Baris kedua
Baris ketiga
deskripsiSatu: >
Semua baris ini
dijadikan satu paragraf
dipisahkan spasi
# Mapping (setara Map/Object)
alamat:
jalan: "Jl. Merdeka No. 1"
kota: Jakarta
kodePos: "10110"
# Sequence (setara List/Array)
bahasa:
- Kotlin
- Java
- Python
# Mapping dalam sequence
pengguna:
- nama: Budi
umur: 25
- nama: Sari
umur: 28
# Inline style (setara JSON)
koordinat: {lat: -6.2, lng: 106.8}
tag: [kotlin, backend, api]
# Anchor dan alias (untuk reuse)
defaultConfig: &default
timeout: 30
retries: 3
production:
<<: *default # merge dari anchor
host: prod.example.com
development:
<<: *default
host: localhost
YAML vs JSON — Kapan Memilih Mana #
| Aspek | YAML | JSON |
|---|---|---|
| Keterbacaan | Lebih mudah dibaca manusia | Lebih verbose |
| Komentar | ✓ Didukung | ✗ Tidak didukung |
| Multiline string | ✓ Native (| dan >) | Escape \n manual |
| Tipe data | Otomatis diinfer | Eksplisit (string harus dikutip) |
| Error-prone | Lebih (indentasi sensitif) | Lebih aman (struktur eksplisit) |
| Cocok untuk | File konfigurasi, CI/CD | API response, data exchange |
| Parser | Lebih kompleks | Lebih sederhana |
SnakeYAML #
SnakeYAML adalah library Java paling populer untuk YAML di JVM. Karena Kotlin berjalan di atas JVM, ia bisa digunakan langsung tanpa adapter khusus.
Setup #
// build.gradle.kts
dependencies {
implementation("org.yaml:snakeyaml:2.2")
}
Membaca YAML ke Map #
import org.yaml.snakeyaml.Yaml
import java.io.File
fun main() {
val yaml = Yaml()
// Baca dari string
val yamlString = """
nama: Budi Santoso
umur: 25
kota: Jakarta
bahasa:
- Kotlin
- Java
alamat:
jalan: Jl. Merdeka No. 1
kodePos: "10110"
""".trimIndent()
val data: Map<String, Any> = yaml.load(yamlString)
println(data["nama"]) // Budi Santoso
println(data["umur"]) // 25
@Suppress("UNCHECKED_CAST")
val bahasa = data["bahasa"] as List<String>
println(bahasa) // [Kotlin, Java]
@Suppress("UNCHECKED_CAST")
val alamat = data["alamat"] as Map<String, String>
println(alamat["kodePos"]) // 10110
// Baca dari file
val dataFile: Map<String, Any> = File("config.yaml").inputStream().use {
yaml.load(it)
}
}
Membaca YAML ke Data Class #
SnakeYAML bisa langsung memetakan YAML ke kelas Kotlin, tapi nama field harus cocok persis:
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.constructor.Constructor
import org.yaml.snakeyaml.LoaderOptions
// Kelas harus punya constructor no-arg untuk SnakeYAML
data class KonfigurasiDatabase(
var host: String = "",
var port: Int = 5432,
var nama: String = "",
var pengguna: String = "",
var sandi: String = "",
var poolMaks: Int = 10
)
data class KonfigurasiAplikasi(
var server: KonfigurasiServer = KonfigurasiServer(),
var database: KonfigurasiDatabase = KonfigurasiDatabase(),
var logging: KonfigurasiLogging = KonfigurasiLogging()
)
data class KonfigurasiServer(
var host: String = "0.0.0.0",
var port: Int = 8080,
var debug: Boolean = false
)
data class KonfigurasiLogging(
var level: String = "INFO",
var file: String? = null
)
fun bacaKonfigurasi(path: String): KonfigurasiAplikasi {
val options = LoaderOptions()
val constructor = Constructor(KonfigurasiAplikasi::class.java, options)
val yaml = Yaml(constructor)
return File(path).inputStream().use { yaml.load(it) }
}
File config.yaml:
server:
host: 0.0.0.0
port: 8080
debug: false
database:
host: db.example.com
port: 5432
nama: myapp_db
pengguna: admin
sandi: rahasia123
poolMaks: 20
logging:
level: INFO
file: /var/log/myapp/app.log
Menulis Objek ke YAML #
import org.yaml.snakeyaml.DumperOptions
import org.yaml.snakeyaml.Yaml
fun tulisYaml(data: Any, path: String) {
val options = DumperOptions().apply {
defaultFlowStyle = DumperOptions.FlowStyle.BLOCK // format blok, bukan inline
isPrettyFlow = true
indent = 2
}
val yaml = Yaml(options)
File(path).bufferedWriter().use { writer ->
yaml.dump(data, writer)
}
}
// Tulis Map ke YAML
val konfigurasi = mapOf(
"server" to mapOf(
"host" to "localhost",
"port" to 8080
),
"database" to mapOf(
"host" to "db.local",
"port" to 5432,
"nama" to "testdb"
)
)
tulisYaml(konfigurasi, "output.yaml")
Multi-Document YAML #
YAML mendukung beberapa dokumen dalam satu file, dipisahkan oleh ---:
val yamlMulti = """
---
nama: Dokumen 1
versi: 1.0
---
nama: Dokumen 2
versi: 2.0
---
nama: Dokumen 3
versi: 3.0
""".trimIndent()
val yaml = Yaml()
val dokumen = yaml.loadAll(yamlMulti)
for (doc in dokumen) {
@Suppress("UNCHECKED_CAST")
val map = doc as Map<String, Any>
println("${map["nama"]} v${map["versi"]}")
}
// Dokumen 1 v1.0
// Dokumen 2 v2.0
// Dokumen 3 v3.0
kaml — YAML dengan kotlinx.serialization #
kaml (Kotlin YAML) adalah library yang mengintegrasikan YAML parsing dengan kotlinx.serialization. Ini memberi kamu type-safety penuh dengan @Serializable dan dukungan semua fitur serialisasi Kotlin.
Setup #
// build.gradle.kts
plugins {
kotlin("plugin.serialization") version "2.0.0"
}
dependencies {
implementation("com.charleskorn.kaml:kaml:0.57.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3")
}
Decode dan Encode dengan kaml #
import com.charleskorn.kaml.Yaml
import com.charleskorn.kaml.YamlConfiguration
import kotlinx.serialization.Serializable
@Serializable
data class KonfigServer(
val host: String,
val port: Int,
val debug: Boolean = false,
val cors: List<String> = emptyList()
)
@Serializable
data class KonfigDatabase(
val host: String,
val port: Int = 5432,
val nama: String,
val pengguna: String,
val sandi: String,
val poolMaks: Int = 10
)
@Serializable
data class Konfigurasi(
val server: KonfigServer,
val database: KonfigDatabase,
val debug: Boolean = false
)
fun main() {
val yaml = Yaml(
configuration = YamlConfiguration(
strictMode = false // abaikan field yang tidak ada di kelas
)
)
val yamlString = """
server:
host: "0.0.0.0"
port: 8080
debug: true
cors:
- "https://app.example.com"
- "https://admin.example.com"
database:
host: "db.example.com"
nama: "myapp"
pengguna: "admin"
sandi: "rahasia"
poolMaks: 20
""".trimIndent()
// Decode YAML → objek
val config = yaml.decodeFromString(Konfigurasi.serializer(), yamlString)
println(config.server.port) // 8080
println(config.database.poolMaks) // 20
println(config.server.cors.size) // 2
// Encode objek → YAML
val outputYaml = yaml.encodeToString(Konfigurasi.serializer(), config)
println(outputYaml)
}
Keunggulan kaml vs SnakeYAML #
// kaml: type-safe, compile-time validation
@Serializable
data class Pengguna(
val nama: String,
val umur: Int,
val email: String,
val aktif: Boolean = true
)
val yaml = Yaml()
val pengguna = yaml.decodeFromString(
Pengguna.serializer(),
"""
nama: Budi
umur: 25
email: [email protected]
""".trimIndent()
)
// Jika 'nama' tidak ada → error yang jelas saat decode
// Field 'aktif' menggunakan default value → tidak perlu ada di YAML
// SnakeYAML: tidak type-safe, error hanya di runtime
// data class harus punya no-arg constructor
// default value tidak selalu bekerja sesuai harapan
Pola Manajemen Konfigurasi #
Ini adalah pola umum yang digunakan di aplikasi production untuk mengelola konfigurasi dari file YAML:
Konfigurasi dengan Environment Override #
import com.charleskorn.kaml.Yaml
import kotlinx.serialization.Serializable
import java.io.File
@Serializable
data class KonfigurasiApp(
val nama: String = "MyApp",
val server: KonfigurasiServer2 = KonfigurasiServer2(),
val database: KonfigurasiDatabase2 = KonfigurasiDatabase2(),
val fitur: KonfigurasiF = KonfigurasiF()
)
@Serializable
data class KonfigurasiServer2(
val host: String = "0.0.0.0",
val port: Int = 8080,
val timeoutDetik: Int = 30
)
@Serializable
data class KonfigurasiDatabase2(
val url: String = "jdbc:postgresql://localhost/myapp",
val pengguna: String = "postgres",
val sandi: String = "",
val poolMin: Int = 2,
val poolMaks: Int = 10
)
@Serializable
data class KonfigurasiF(
val registrasiBuka: Boolean = true,
val maksUploadMb: Int = 10,
val modeMaintenance: Boolean = false
)
object PengelolaKonfigurasi {
private lateinit var konfigurasi: KonfigurasiApp
fun muat(pathFile: String = "config.yaml"): KonfigurasiApp {
val yaml = Yaml(configuration = com.charleskorn.kaml.YamlConfiguration(strictMode = false))
// Baca dari file utama
val file = File(pathFile)
val config = if (file.exists()) {
file.inputStream().use {
yaml.decodeFromStream(KonfigurasiApp.serializer(), it)
}
} else {
println("File konfigurasi tidak ditemukan, menggunakan default")
KonfigurasiApp()
}
// Override dari environment variable
val configFinal = config.copy(
database = config.database.copy(
url = System.getenv("DATABASE_URL") ?: config.database.url,
pengguna = System.getenv("DATABASE_USER") ?: config.database.pengguna,
sandi = System.getenv("DATABASE_PASSWORD") ?: config.database.sandi
),
server = config.server.copy(
port = System.getenv("PORT")?.toIntOrNull() ?: config.server.port
)
)
konfigurasi = configFinal
return configFinal
}
fun dapatkan(): KonfigurasiApp {
check(::konfigurasi.isInitialized) { "Konfigurasi belum dimuat" }
return konfigurasi
}
}
fun main() {
val config = PengelolaKonfigurasi.muat("config.yaml")
println("Server berjalan di ${config.server.host}:${config.server.port}")
println("Database: ${config.database.url}")
}
Konfigurasi Per Environment #
Pola umum: satu file base + file override per environment:
config/
├── application.yaml ← konfigurasi base (nilai default)
├── application-dev.yaml ← override untuk development
├── application-staging.yaml ← override untuk staging
└── application-prod.yaml ← override untuk production
fun muatKonfigurasiDenganEnvironment(): KonfigurasiApp {
val env = System.getenv("APP_ENV") ?: "dev"
val yaml = Yaml(configuration = com.charleskorn.kaml.YamlConfiguration(strictMode = false))
// Muat base config
val base = File("config/application.yaml").takeIf { it.exists() }
?.inputStream()
?.use { yaml.decodeFromStream(KonfigurasiApp.serializer(), it) }
?: KonfigurasiApp()
// Muat env-specific config dan merge (sederhana: override penuh)
val envFile = File("config/application-$env.yaml")
return if (envFile.exists()) {
envFile.inputStream().use {
yaml.decodeFromStream(KonfigurasiApp.serializer(), it)
}
} else {
println("Tidak ada konfigurasi untuk env '$env', pakai base")
base
}
}
Validasi Konfigurasi YAML #
Setelah membaca konfigurasi, selalu validasi nilainya sebelum digunakan:
fun KonfigurasiApp.validasi(): List<String> {
val masalah = mutableListOf<String>()
if (nama.isBlank()) masalah.add("Nama aplikasi tidak boleh kosong")
if (server.port !in 1..65535) masalah.add("Port server tidak valid: ${server.port}")
if (server.timeoutDetik <= 0) masalah.add("Timeout harus positif")
if (database.url.isBlank()) masalah.add("URL database tidak boleh kosong")
if (database.poolMin > database.poolMaks) masalah.add("Pool min > pool maks")
if (fitur.maksUploadMb <= 0) masalah.add("Maks upload harus positif")
return masalah
}
fun main() {
val config = PengelolaKonfigurasi.muat()
val masalah = config.validasi()
if (masalah.isNotEmpty()) {
println("Konfigurasi tidak valid:")
masalah.forEach { println(" • $it") }
System.exit(1) // hentikan aplikasi
}
println("Konfigurasi valid. Memulai aplikasi...")
}
Penanganan Error YAML #
import com.charleskorn.kaml.YamlException
fun bacaKonfigurasiAman(path: String): Result<KonfigurasiApp> {
return runCatching {
val yaml = Yaml(configuration = com.charleskorn.kaml.YamlConfiguration(strictMode = false))
File(path).inputStream().use {
yaml.decodeFromStream(KonfigurasiApp.serializer(), it)
}
}
}
fun main() {
val hasil = bacaKonfigurasiAman("config.yaml")
hasil
.onSuccess { config ->
println("Konfigurasi berhasil dimuat: ${config.nama}")
}
.onFailure { e ->
when (e) {
is YamlException -> println("YAML tidak valid: ${e.message}")
is java.io.FileNotFoundException -> println("File tidak ditemukan: config.yaml")
else -> println("Error tidak terduga: ${e.message}")
}
System.exit(1)
}
}
Ringkasan #
- kaml untuk proyek Kotlin modern — integrasi dengan kotlinx.serialization memberikan type-safety penuh, null-safety, dan default value yang bekerja benar. Cocok untuk proyek baru.
- SnakeYAML untuk interop Java dan fleksibilitas — lebih matur, bisa membaca YAML ke
Map<String, Any>tanpa mendefinisikan kelas dulu. Cocok ketika struktur YAML tidak diketahui sebelumnya.- YAML sensitif terhadap indentasi — gunakan spasi (bukan tab) untuk indentasi, konsisten dalam satu file. Dua spasi adalah standar yang paling umum.
- Environment variable untuk nilai sensitif — jangan hardcode password, API key, atau URL production di file YAML yang masuk ke version control. Baca dari environment variable, dengan nilai default dari file YAML sebagai fallback.
- Validasi konfigurasi saat startup — periksa semua nilai konfigurasi sebelum aplikasi mulai melayani request. Lebih baik crash dengan pesan error yang jelas saat startup daripada crash secara misterius di tengah operasi.
- Pisahkan konfigurasi per environment —
application.yamluntuk default,application-dev.yamluntuk development,application-prod.yamluntuk production. Jangan campur nilai antar environment.- Komentar adalah dokumentasi — manfaatkan kemampuan YAML untuk komentar. Dokumentasikan arti setiap nilai konfigurasi langsung di file YAML.
- Multi-document YAML dengan
---— berguna untuk mendefinisikan beberapa konfigurasi terkait dalam satu file, misalnya beberapa definisi service atau beberapa konfigurasi test.