YAML

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 #

AspekYAMLJSON
KeterbacaanLebih mudah dibaca manusiaLebih verbose
Komentar✓ Didukung✗ Tidak didukung
Multiline string✓ Native (| dan >)Escape \n manual
Tipe dataOtomatis diinferEksplisit (string harus dikutip)
Error-proneLebih (indentasi sensitif)Lebih aman (struktur eksplisit)
Cocok untukFile konfigurasi, CI/CDAPI response, data exchange
ParserLebih kompleksLebih 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
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 environmentapplication.yaml untuk default, application-dev.yaml untuk development, application-prod.yaml untuk 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.

← Sebelumnya: JSON   Berikutnya: MySQL →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact