YAML

YAML #

YAML (YAML Ain’t Markup Language) adalah format serialisasi data yang dirancang untuk mudah dibaca manusia. Ia mendominasi dunia konfigurasi — Docker Compose, Kubernetes manifests, GitHub Actions, Ansible playbook, dan banyak lagi semuanya menggunakan YAML. Di Rust, dukungan YAML disediakan oleh crate serde_yaml yang bekerja persis seperti serde_json: menggunakan trait Serialize dan Deserialize dari serde, sehingga struct yang sudah bekerja dengan JSON bisa langsung digunakan untuk YAML tanpa modifikasi. Artikel ini membahas pembacaan dan penulisan YAML, anotasi kontrol format, YAML Value dinamis, pola umum untuk file konfigurasi, dan perbandingan dengan JSON dan TOML.

Instalasi #

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9"

Serialize dan Deserialize Dasar #

Struct ke YAML #

use serde::{Deserialize, Serialize};
use serde_yaml;

#[derive(Serialize, Deserialize, Debug)]
struct KonfigServer {
    host: String,
    port: u16,
    workers: u32,
    debug: bool,
    database_url: String,
}

fn main() {
    let config = KonfigServer {
        host: String::from("0.0.0.0"),
        port: 8080,
        workers: 4,
        debug: false,
        database_url: String::from("postgres://localhost/produksi"),
    };

    // Serialize ke YAML string
    let yaml_str = serde_yaml::to_string(&config).unwrap();
    println!("{}", yaml_str);
    // host: 0.0.0.0
    // port: 8080
    // workers: 4
    // debug: false
    // database_url: postgres://localhost/produksi

    // Serialize ke writer (file, stdout, dll.)
    let mut output = Vec::new();
    serde_yaml::to_writer(&mut output, &config).unwrap();
    println!("YAML bytes: {}", output.len());
}

YAML ke Struct #

use serde::Deserialize;
use serde_yaml;

#[derive(Deserialize, Debug)]
struct Aplikasi {
    nama: String,
    versi: String,
    port: u16,
    fitur: Vec<String>,
    batas: Batas,
}

#[derive(Deserialize, Debug)]
struct Batas {
    maks_koneksi: u32,
    timeout_detik: u64,
    maks_body_mb: f64,
}

fn main() {
    let yaml_str = "
nama: Aplikasi Rust
versi: '2.1.0'
port: 3000
fitur:
  - auth
  - caching
  - metrics
batas:
  maks_koneksi: 100
  timeout_detik: 30
  maks_body_mb: 10.5
";

    // Deserialize dari &str
    let app: Aplikasi = serde_yaml::from_str(yaml_str).unwrap();
    println!("Nama: {}", app.nama);
    println!("Fitur: {:?}", app.fitur);
    println!("Maks koneksi: {}", app.batas.maks_koneksi);

    // Deserialize dari file
    let file = std::fs::File::open("config.yaml");
    if let Ok(f) = file {
        let config: Aplikasi = serde_yaml::from_reader(f).unwrap();
        println!("Dari file: {}", config.nama);
    }

    // Error handling yang informatif
    let yaml_rusak = "port: bukan_angka";
    match serde_yaml::from_str::<Aplikasi>(yaml_rusak) {
        Ok(_) => println!("Berhasil"),
        Err(e) => println!("Error: {}", e),
    }
}

Konfigurasi File yang Realistis #

YAML paling sering digunakan untuk file konfigurasi. Berikut pola yang umum digunakan di aplikasi produksi:

use serde::{Deserialize, Serialize};
use std::fs;

#[derive(Debug, Deserialize, Serialize)]
struct KonfigLengkap {
    aplikasi: KonfigAplikasi,
    database: KonfigDatabase,
    redis: KonfigRedis,
    log: KonfigLog,
    keamanan: KonfigKeamanan,
}

#[derive(Debug, Deserialize, Serialize)]
struct KonfigAplikasi {
    nama: String,
    host: String,
    port: u16,
    #[serde(default)]
    debug: bool,
    #[serde(default = "versi_default")]
    versi: String,
}

fn versi_default() -> String {
    String::from("0.1.0")
}

#[derive(Debug, Deserialize, Serialize)]
struct KonfigDatabase {
    url: String,
    maks_koneksi: u32,
    #[serde(default = "timeout_default")]
    timeout_detik: u64,
    ssl: bool,
}

fn timeout_default() -> u64 { 30 }

#[derive(Debug, Deserialize, Serialize)]
struct KonfigRedis {
    url: String,
    ttl_detik: u64,
}

#[derive(Debug, Deserialize, Serialize)]
struct KonfigLog {
    level: String,
    format: String,
    file: Option<String>,
}

#[derive(Debug, Deserialize, Serialize)]
struct KonfigKeamanan {
    jwt_rahasia: String,
    bcrypt_cost: u8,
    cors_origins: Vec<String>,
}

fn muat_konfigurasi(path: &str) -> Result<KonfigLengkap, Box<dyn std::error::Error>> {
    let isi = fs::read_to_string(path)?;
    let config: KonfigLengkap = serde_yaml::from_str(&isi)?;
    Ok(config)
}

fn main() {
    // Contoh YAML yang sesuai dengan struct di atas
    let yaml_contoh = r#"
aplikasi:
  nama: "API Server"
  host: "0.0.0.0"
  port: 8080
  debug: true

database:
  url: "postgres://user:pass@localhost:5432/db"
  maks_koneksi: 20
  ssl: true

redis:
  url: "redis://localhost:6379"
  ttl_detik: 3600

log:
  level: "info"
  format: "json"
  file: "/var/log/app.log"

keamanan:
  jwt_rahasia: "rahasia-super-panjang"
  bcrypt_cost: 12
  cors_origins:
    - "https://app.contoh.com"
    - "https://admin.contoh.com"
"#;

    let config: KonfigLengkap = serde_yaml::from_str(yaml_contoh).unwrap();
    println!("Server: {}:{}", config.aplikasi.host, config.aplikasi.port);
    println!("DB: {}", config.database.url);
    println!("CORS: {:?}", config.keamanan.cors_origins);

    // Tulis konfigurasi kembali ke YAML
    let yaml_kembali = serde_yaml::to_string(&config).unwrap();
    println!("\n--- Konfigurasi sebagai YAML ---\n{}", yaml_kembali);
}

Anotasi #[serde(...)] untuk YAML #

Semua anotasi serde yang bekerja untuk JSON juga bekerja untuk YAML — mereka adalah bagian dari framework serde, bukan spesifik per format:

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]  // snake_case Rust → kebab-case YAML
struct KonfigDocker {
    image_name: String,         // → image-name
    container_port: u16,        // → container-port
    restart_policy: String,     // → restart-policy
    environment_vars: Vec<String>, // → environment-vars
}

#[derive(Serialize, Deserialize, Debug)]
struct Layanan {
    nama: String,
    aktif: bool,

    // Jika tidak ada di YAML, gunakan default
    #[serde(default)]
    replika: u32,

    // Tidak disertakan jika None
    #[serde(skip_serializing_if = "Option::is_none")]
    catatan: Option<String>,

    // Rename spesifik
    #[serde(rename = "healthcheck-url")]
    url_healthcheck: String,
}

fn main() {
    let layanan = Layanan {
        nama: String::from("api-server"),
        aktif: true,
        replika: 3,
        catatan: None,
        url_healthcheck: String::from("/health"),
    };

    let yaml = serde_yaml::to_string(&layanan).unwrap();
    println!("{}", yaml);
    // nama: api-server
    // aktif: true
    // replika: 3
    // healthcheck-url: /health
    // (catatan tidak muncul karena None)

    // Deserialize dengan field yang tidak ada menggunakan default
    let yaml_minimal = "
nama: worker
aktif: false
healthcheck-url: /ping
";
    let layanan2: Layanan = serde_yaml::from_str(yaml_minimal).unwrap();
    println!("Replika default: {}", layanan2.replika); // 0
}

YAML Value Dinamis #

Seperti serde_json::Value, serde_yaml menyediakan serde_yaml::Value untuk YAML yang strukturnya tidak diketahui saat kompilasi:

use serde_yaml::Value;

fn main() {
    let yaml_str = "
server:
  host: localhost
  port: 8080
  tags:
    - web
    - api
database:
  primary:
    url: postgres://localhost/db
    pool: 10
  replica:
    url: postgres://replica/db
    pool: 5
";

    let nilai: Value = serde_yaml::from_str(yaml_str).unwrap();

    // Akses field dengan indexing (sama seperti serde_json::Value)
    println!("Host: {}", nilai["server"]["host"]);
    println!("Port: {}", nilai["server"]["port"]);

    // Iterasi sequence (array)
    if let Some(tags) = nilai["server"]["tags"].as_sequence() {
        println!("Tags:");
        for tag in tags {
            println!("  - {}", tag.as_str().unwrap_or(""));
        }
    }

    // Iterasi mapping (object)
    if let Some(db) = nilai["database"].as_mapping() {
        for (kunci, val) in db {
            println!("DB {}: pool={}", kunci.as_str().unwrap_or("?"),
                     val["pool"].as_i64().unwrap_or(0));
        }
    }

    // Buat YAML dari Value secara programatik
    let mut map = serde_yaml::Mapping::new();
    map.insert(Value::String("host".into()), Value::String("localhost".into()));
    map.insert(Value::String("port".into()), Value::Number(3000.into()));

    let mut seq = serde_yaml::Sequence::new();
    seq.push(Value::String("fitur-a".into()));
    seq.push(Value::String("fitur-b".into()));
    map.insert(Value::String("fitur".into()), Value::Sequence(seq));

    let nilai_baru = Value::Mapping(map);
    println!("{}", serde_yaml::to_string(&nilai_baru).unwrap());
}

Fitur YAML yang Tidak Ada di JSON #

Multi-Document dalam Satu File #

YAML mendukung banyak dokumen dalam satu file, dipisahkan oleh ---:

use serde::Deserialize;
use serde_yaml;

#[derive(Deserialize, Debug)]
struct Sumber {
    nama: String,
    tipe: String,
}

fn main() {
    let yaml_multi = "
---
nama: database-primary
tipe: postgresql
---
nama: cache-server
tipe: redis
---
nama: message-broker
tipe: rabbitmq
";

    // Deserialize multi-document
    let iter = serde_yaml::Deserializer::from_str(yaml_multi);
    for dokumen in iter {
        let sumber = Sumber::deserialize(dokumen).unwrap();
        println!("{}: {}", sumber.nama, sumber.tipe);
    }
}

Komentar di YAML #

Ini fitur penting yang tidak dimiliki JSON — YAML mendukung komentar dengan #. Sangat berguna untuk file konfigurasi:

# File konfigurasi utama
# Dibuat: 2024-08-24

server:
  host: 0.0.0.0  # Dengarkan di semua interface
  port: 8080     # Port default, ubah jika conflict

database:
  # Format: postgres://user:password@host:port/dbname
  url: postgres://admin:rahasia@localhost:5432/produksi
  maks_koneksi: 20  # Sesuaikan dengan spesifikasi server

Komentar diabaikan saat parsing — tidak muncul saat YAML di-serialize kembali.

Anchor dan Alias — Hindari Duplikasi #

YAML punya mekanisme anchor (&nama) dan alias (*nama) untuk reuse nilai:

# Define base config sebagai anchor
default_db: &default_db
  maks_koneksi: 10
  timeout: 30
  ssl: true

database:
  primary:
    <<: *default_db  # merge key — salin semua field dari anchor
    url: postgres://primary/db
    maks_koneksi: 20  # override field yang di-merge

  replica:
    <<: *default_db  # reuse konfigurasi default
    url: postgres://replica/db
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct DbConfig {
    url: String,
    maks_koneksi: u32,
    timeout: u32,
    ssl: bool,
}

#[derive(Deserialize, Debug)]
struct Database {
    primary: DbConfig,
    replica: DbConfig,
}

fn main() {
    let yaml = "
default_db: &default_db
  maks_koneksi: 10
  timeout: 30
  ssl: true

database:
  primary:
    <<: *default_db
    url: postgres://primary/db
    maks_koneksi: 20
  replica:
    <<: *default_db
    url: postgres://replica/db
";

    // serde_yaml menangani anchor/alias secara otomatis
    let nilai: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
    println!("Primary maks_koneksi: {}", nilai["database"]["primary"]["maks_koneksi"]);
    println!("Replica maks_koneksi: {}", nilai["database"]["replica"]["maks_koneksi"]);
    println!("Primary ssl: {}", nilai["database"]["primary"]["ssl"]);
}

Include File Config dengan include_str! #

Untuk menyertakan file YAML sebagai bagian dari binary (embed), gunakan macro include_str!:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct KonfigDefault {
    versi: String,
    fitur_aktif: Vec<String>,
    batas_rate: u32,
}

// File konfigurasi di-embed ke dalam binary saat kompilasi
const KONFIGURASI_DEFAULT: &str = include_str!("../config/default.yaml");

fn main() {
    let config: KonfigDefault = serde_yaml::from_str(KONFIGURASI_DEFAULT).unwrap();
    println!("Versi default: {}", config.versi);
    println!("Fitur: {:?}", config.fitur_aktif);

    // Pola umum: config default dari binary, override dari file eksternal
    let config_override = std::fs::read_to_string("config.local.yaml")
        .unwrap_or_default();

    if !config_override.is_empty() {
        let local: KonfigDefault = serde_yaml::from_str(&config_override).unwrap();
        println!("Override lokal: {}", local.versi);
    }
}

Konversi YAML ↔ JSON #

Karena keduanya menggunakan serde, konversi antara YAML dan JSON sangat mudah:

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct Konfigurasi {
    nama: String,
    port: u16,
    fitur: Vec<String>,
}

fn yaml_ke_json(yaml_str: &str) -> Result<String, Box<dyn std::error::Error>> {
    // Parse YAML ke Value
    let nilai: serde_yaml::Value = serde_yaml::from_str(yaml_str)?;
    // Konversi ke serde_json::Value via serde
    let json_nilai: serde_json::Value = serde_json::to_value(nilai)?;
    // Serialize ke JSON string
    Ok(serde_json::to_string_pretty(&json_nilai)?)
}

fn json_ke_yaml(json_str: &str) -> Result<String, Box<dyn std::error::Error>> {
    let nilai: serde_json::Value = serde_json::from_str(json_str)?;
    let yaml_nilai: serde_yaml::Value = serde_yaml::to_value(nilai)?;
    Ok(serde_yaml::to_string(&yaml_nilai)?)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let yaml_str = "
nama: Server Produksi
port: 443
fitur:
  - tls
  - caching
  - compression
";

    let json_hasil = yaml_ke_json(yaml_str)?;
    println!("JSON:\n{}", json_hasil);

    let json_str = r#"{"nama": "Dev Server", "port": 3000, "fitur": ["debug"]}"#;
    let yaml_hasil = json_ke_yaml(json_str)?;
    println!("YAML:\n{}", yaml_hasil);

    // Atau lebih langsung: roundtrip via typed struct
    let config: Konfigurasi = serde_yaml::from_str(yaml_str).unwrap();
    let sebagai_json = serde_json::to_string_pretty(&config).unwrap();
    println!("Via struct:\n{}", sebagai_json);

    Ok(())
}

YAML vs JSON vs TOML — Kapan Memilih #

AspekYAMLJSONTOML
KeterbacaanSangat tinggiSedangTinggi
KomentarYa (#)TidakYa (#)
VerboseRendahSedangSedang
Kompleksitas parserTinggiRendahSedang
Multi-documentYa (---)TidakTidak
Anchor/aliasYaTidakTidak
Error saat indentasi salahYaTidakTidak
Cocok untukKonfigurasi kompleks, DevOpsAPI, data exchangeCargo.toml, konfigurasi app
Gunakan YAML jika:
  ✓ File konfigurasi yang sering diedit manusia (Docker, K8s, CI/CD)
  ✓ Perlu komentar untuk menjelaskan nilai
  ✓ Konfigurasi hierarkis yang dalam
  ✓ Perlu anchor/alias untuk hindari duplikasi

Gunakan JSON jika:
  ✓ API response/request — standar de facto
  ✓ Pertukaran data antar sistem
  ✓ Tidak perlu komentar
  ✓ Parser yang lebih cepat dan lebih sederhana

Gunakan TOML jika:
  ✓ Konfigurasi aplikasi Rust (Cargo.toml, rust-toolchain.toml)
  ✓ File konfigurasi sederhana hingga menengah
  ✓ Developer lebih familiar dengan format INI-like

Ringkasan #

  • serde_yaml menggunakan trait serde yang sama dengan serde_json — struct yang sudah #[derive(Serialize, Deserialize)] langsung bisa digunakan tanpa modifikasi.
  • from_str untuk string, from_reader untuk filefrom_reader lebih efisien untuk file besar karena streaming.
  • Semua anotasi #[serde(...)] bekerjarename, rename_all, skip_serializing_if, default, skip semuanya valid untuk YAML seperti untuk JSON.
  • serde_yaml::Value untuk YAML dinamis — akses via ["kunci"], .as_sequence(), .as_mapping(), .as_str(), .as_i64().
  • Multi-document dengan serde_yaml::Deserializer::from_str — parse beberapa dokumen YAML yang dipisahkan --- dalam satu string.
  • Anchor dan alias (&nama dan *nama) ditangani otomatis oleh serde_yaml — kamu bisa menggunakannya di file YAML tanpa kode tambahan.
  • include_str! untuk menyertakan file YAML ke dalam binary saat kompilasi — cocok untuk konfigurasi default yang tidak boleh hilang.
  • Konversi YAML ↔ JSON mudah — via typed struct (from_yamlto_json) atau via Value intermediate.
  • YAML lebih cocok dari JSON untuk file konfigurasi karena mendukung komentar, lebih mudah dibaca, dan lebih sedikit karakter boilerplate (tidak ada tanda kutip untuk kunci, tidak ada koma).

← Sebelumnya: JSON   Berikutnya: MySQL →

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