YAML

YAML #

YAML (YAML Ain’t Markup Language) adalah format serialisasi data yang dirancang untuk mudah dibaca manusia — menggunakan indentasi dan simbol minimal dibanding JSON yang penuh tanda kurung dan kutip. Di ekosistem Dart, YAML digunakan di mana-mana: pubspec.yaml, analysis_options.yaml, build.yaml, dan file konfigurasi aplikasi. Package yaml menyediakan parser untuk membaca YAML, sementara penulisan YAML bisa dilakukan dengan yaml_writer. Artikel ini membahas cara membaca dan menulis YAML secara efektif, konversi ke model kelas, dan kapan YAML lebih tepat digunakan dibanding JSON.

Sintaks YAML — Referensi Cepat #

Sebelum masuk ke kode Dart, penting memahami sintaks YAML yang akan diparse:

# Komentar dimulai dengan #

# Scalar — nilai primitif
nama: Budi Santoso
umur: 25
tinggi: 1.75
aktif: true
kosong: null            # atau ~

# String — tanda kutip opsional, wajib jika ada karakter khusus
kota: Jakarta
alamat: "Jl. Merdeka No. 1, Jakarta"
pesan: 'Ini "tanda kutip" dalam string'

# Multi-line string
deskripsi: |
  Ini baris pertama.
  Ini baris kedua.
  Newline dipertahankan.  

ringkasan: >
  Ini paragraf panjang yang
  di-wrap. Newline menjadi spasi,
  kecuali paragraf baru.  

# List (urutan)
buah:
  - apel
  - jeruk
  - mangga

# List inline
warna: [merah, hijau, biru]

# Map (mapping)
database:
  host: localhost
  port: 5432
  nama: mydb

# Map inline
koordinat: {lat: -6.2, lng: 106.8}

# Nested — list of maps
pengguna:
  - nama: Budi
    email: [email protected]
    peran: admin
  - nama: Siti
    email: [email protected]
    peran: user

# Anchor (&) dan Alias (*) — reuse nilai
default_db: &db_default
  host: localhost
  port: 5432

produksi:
  <<: *db_default       # merge dari anchor
  host: prod.example.com  # override field tertentu
  nama: prod_db

# Multi-document dalam satu file (dipisah ---)
---
dokumen: pertama
---
dokumen: kedua

Setup Package yaml #

dart pub add yaml
# pubspec.yaml
dependencies:
  yaml: ^3.1.2

loadYaml — Parsing Dasar #

import 'package:yaml/yaml.dart';

void main() {
  final yamlString = '''
nama: Budi Santoso
umur: 25
aktif: true
hobi:
  - membaca
  - coding
  - hiking
alamat:
  kota: Jakarta
  kodePos: '10110'
''';

  final doc = loadYaml(yamlString);

  // Hasil loadYaml adalah YamlMap — mirip Map tapi bukan Map Dart biasa
  print(doc.runtimeType);    // YamlMap

  // Akses nilai — mirip seperti Map
  print(doc['nama']);        // Budi Santoso
  print(doc['umur']);        // 25 (int)
  print(doc['aktif']);       // true (bool)

  // List
  final hobi = doc['hobi'] as YamlList;
  for (final h in hobi) {
    print(h); // membaca, coding, hiking
  }

  // Nested map
  final alamat = doc['alamat'] as YamlMap;
  print(alamat['kota']);     // Jakarta
  print(alamat['kodePos']);  // 10110 (String karena pakai tanda kutip di YAML)
}

YamlMap vs Map Dart #

loadYaml mengembalikan YamlMap dan YamlList, bukan Map dan List Dart biasa. Keduanya read-only — tidak bisa dimodifikasi. Untuk penggunaan yang membutuhkan Map/List biasa, konversi terlebih dahulu:

import 'package:yaml/yaml.dart';

// Konversi rekursif YamlMap/YamlList ke tipe Dart native
dynamic yamlKeDart(dynamic node) {
  if (node is YamlMap) {
    return Map<String, dynamic>.fromEntries(
      node.entries.map(
        (e) => MapEntry(e.key.toString(), yamlKeDart(e.value)),
      ),
    );
  } else if (node is YamlList) {
    return node.map(yamlKeDart).toList();
  }
  return node; // scalar — String, int, double, bool, null
}

void main() {
  final doc = loadYaml('nama: Budi\numur: 25');
  final dartMap = yamlKeDart(doc) as Map<String, dynamic>;

  // Sekarang bisa dimodifikasi
  dartMap['email'] = '[email protected]';
  print(dartMap); // {nama: Budi, umur: 25, email: [email protected]}
}

Membaca YAML dari File #

import 'dart:io';
import 'package:yaml/yaml.dart';

Future<dynamic> bacaYaml(String path) async {
  final file = File(path);
  if (!await file.exists()) {
    throw FileSystemException('File YAML tidak ditemukan', path);
  }
  final konten = await file.readAsString();
  return loadYaml(konten);
}

// config.yaml:
// server:
//   host: localhost
//   port: 8080
//   ssl: false
// database:
//   url: postgresql://localhost/mydb
//   pool_size: 10

Future<void> main() async {
  final config = await bacaYaml('config.yaml');

  final host = config['server']['host'] as String;
  final port = config['server']['port'] as int;
  final dbUrl = config['database']['url'] as String;

  print('Server: $host:$port');
  print('Database: $dbUrl');
}

Parsing ke Model Kelas #

Pola yang sama seperti JSON — buat factory constructor fromYaml:

import 'package:yaml/yaml.dart';

class KonfigurasiServer {
  final String host;
  final int port;
  final bool ssl;
  final Duration timeout;

  const KonfigurasiServer({
    required this.host,
    required this.port,
    required this.ssl,
    required this.timeout,
  });

  factory KonfigurasiServer.fromYaml(YamlMap yaml) {
    return KonfigurasiServer(
      host: yaml['host'] as String? ?? 'localhost',
      port: yaml['port'] as int? ?? 8080,
      ssl: yaml['ssl'] as bool? ?? false,
      timeout: Duration(
        seconds: yaml['timeout_detik'] as int? ?? 30,
      ),
    );
  }

  @override
  String toString() =>
      'KonfigurasiServer(${ssl ? "https" : "http"}://$host:$port, timeout: ${timeout.inSeconds}s)';
}

class KonfigurasiAplikasi {
  final KonfigurasiServer server;
  final String namaAplikasi;
  final String lingkungan;
  final List<String> fiturAktif;

  const KonfigurasiAplikasi({
    required this.server,
    required this.namaAplikasi,
    required this.lingkungan,
    required this.fiturAktif,
  });

  factory KonfigurasiAplikasi.fromYaml(YamlMap yaml) {
    final fiturRaw = yaml['fitur_aktif'];
    final fitur = fiturRaw is YamlList
        ? fiturRaw.map((f) => f as String).toList()
        : <String>[];

    return KonfigurasiAplikasi(
      namaAplikasi: yaml['nama'] as String,
      lingkungan: yaml['lingkungan'] as String? ?? 'development',
      server: KonfigurasiServer.fromYaml(yaml['server'] as YamlMap),
      fiturAktif: fitur,
    );
  }
}

// Penggunaan
Future<void> main() async {
  final yamlString = await File('app_config.yaml').readAsString();
  final doc = loadYaml(yamlString) as YamlMap;
  final config = KonfigurasiAplikasi.fromYaml(doc);

  print(config.namaAplikasi);
  print(config.server);
  print('Fitur: ${config.fiturAktif.join(', ')}');
}

Multi-Document YAML #

Satu file YAML bisa berisi beberapa dokumen yang dipisahkan ---:

import 'package:yaml/yaml.dart';

void main() {
  final yamlMultiDoc = '''
---
nama: Budi
peran: admin
---
nama: Siti
peran: user
---
nama: Andi
peran: developer
''';

  // loadYamlDocuments — parsing semua dokumen sekaligus
  final dokumen = loadYamlDocuments(yamlMultiDoc);

  for (final doc in dokumen) {
    final map = doc.contents as YamlMap;
    print('${map['nama']}: ${map['peran']}');
  }
  // Budi: admin
  // Siti: user
  // Andi: developer
}

Package yaml hanya untuk membaca. Untuk menulis YAML, gunakan yaml_writer:

dart pub add yaml_writer
import 'package:yaml_writer/yaml_writer.dart';

void main() {
  final data = {
    'nama': 'Aplikasi Toko',
    'versi': '1.0.0',
    'server': {
      'host': 'localhost',
      'port': 8080,
    },
    'fitur': ['autentikasi', 'pembayaran', 'notifikasi'],
    'database': {
      'url': 'postgresql://localhost/toko',
      'pool': 10,
    },
  };

  final writer = YamlWriter();
  final output = writer.write(data);
  print(output);
}
// Output:
// nama: Aplikasi Toko
// versi: 1.0.0
// server:
//   host: localhost
//   port: 8080
// fitur:
//   - autentikasi
//   - pembayaran
//   - notifikasi
// database:
//   url: "postgresql://localhost/toko"
//   pool: 10

Menyimpan ke file:

import 'dart:io';
import 'package:yaml_writer/yaml_writer.dart';

Future<void> simpanKonfigurasi(Map<String, dynamic> data, String path) async {
  final writer = YamlWriter();
  final yamlString = writer.write(data);
  await File(path).writeAsString(yamlString);
  print('Konfigurasi disimpan ke $path');
}

YAML vs JSON vs TOML — Kapan Menggunakan Masing-masing #

flowchart TD
    A{Kebutuhan format data?} --> B{Dibaca/diedit\nmanusia secara langsung?}
    B -- Tidak --> C[JSON\nuntuk API dan data transfer]
    B -- Ya --> D{Ada komentar\nyang perlu?}
    D -- Tidak --> E{Struktur\nsederhana?}
    E -- Ya --> F[JSON atau YAML]
    E -- Tidak --> G[YAML\nuntuk konfigurasi kompleks]
    D -- Ya --> H{Preferensi sintaks?}
    H -- Indentasi --> G
    H -- Key = value --> I[TOML\nuntuk konfigurasi sederhana]
Aspek YAML JSON TOML
Keterbacaan manusia ✓✓✓ Sangat baik ✓✓ Baik ✓✓✓ Sangat baik
Komentar ✓ Ya (#) ✗ Tidak ✓ Ya (#)
Tipe data Kaya (date, binary) Terbatas Kaya (date, array)
Verbose Minimal Sedang Minimal
Error prone Tinggi (whitespace) Rendah Rendah
Cocok untuk Konfigurasi kompleks, CI/CD API, data transfer Konfigurasi sederhana
Dukungan Dart Package yaml Bawaan (dart:convert) Package toml
// Hal yang sama dalam tiga format:

// YAML
server:
  host: localhost
  port: 8080
fitur:
  - auth
  - payment

// JSON
{
  "server": {"host": "localhost", "port": 8080},
  "fitur": ["auth", "payment"]
}

// TOML
[server]
host = "localhost"
port = 8080
fitur = ["auth", "payment"]

Use Case: Konfigurasi Aplikasi yang Terstruktur #

YAML sangat umum untuk konfigurasi multi-environment:

# config/base.yaml — konfigurasi dasar
app:
  nama: Aplikasi Toko
  versi: 1.0.0
  debug: false

logging:
  level: info
  format: json

database:
  pool_size: 5
  timeout_detik: 30
# config/development.yaml — override untuk development
app:
  debug: true

logging:
  level: debug
  format: text

database:
  url: postgresql://localhost/toko_dev
  pool_size: 2
import 'dart:io';
import 'package:yaml/yaml.dart';

// Loader konfigurasi multi-environment
Future<Map<String, dynamic>> muatKonfigurasi(String lingkungan) async {
  // Muat base config
  final base = await _bacaYaml('config/base.yaml');

  // Muat environment config
  final envPath = 'config/$lingkungan.yaml';
  final envFile = File(envPath);
  if (!await envFile.exists()) {
    return base;
  }

  final env = await _bacaYaml(envPath);

  // Deep merge — env override base
  return _deepMerge(base, env);
}

Future<Map<String, dynamic>> _bacaYaml(String path) async {
  final yaml = loadYaml(await File(path).readAsString());
  return _yamlKeDart(yaml) as Map<String, dynamic>;
}

// Deep merge dua Map — nilai kanan menimpa kiri, nested Map di-merge
Map<String, dynamic> _deepMerge(
    Map<String, dynamic> base, Map<String, dynamic> override) {
  final hasil = Map<String, dynamic>.from(base);
  for (final entry in override.entries) {
    final baseValue = base[entry.key];
    final overrideValue = entry.value;

    if (baseValue is Map<String, dynamic> && overrideValue is Map<String, dynamic>) {
      hasil[entry.key] = _deepMerge(baseValue, overrideValue);
    } else {
      hasil[entry.key] = overrideValue;
    }
  }
  return hasil;
}

dynamic _yamlKeDart(dynamic node) {
  if (node is YamlMap) {
    return Map<String, dynamic>.fromEntries(
      node.entries.map((e) => MapEntry(e.key.toString(), _yamlKeDart(e.value))),
    );
  } else if (node is YamlList) {
    return node.map(_yamlKeDart).toList();
  }
  return node;
}

// Penggunaan
Future<void> main() async {
  final lingkungan = Platform.environment['APP_ENV'] ?? 'development';
  final config = await muatKonfigurasi(lingkungan);

  print('Lingkungan: $lingkungan');
  print('Debug mode: ${config['app']['debug']}');
  print('Database URL: ${config['database']['url']}');
}

Anti-Pattern YAML #

Akses Tanpa Validasi Tipe #

// ANTI-PATTERN: asumsi tipe tanpa cast atau validasi
final doc = loadYaml(yamlString);
final port = doc['server']['port'] * 2; // ✗ dynamic — bisa crash

// BENAR: cast eksplisit dengan fallback
final port = (doc['server']['port'] as int?) ?? 8080;
final portDikalikan = port * 2;

YAML untuk Data yang Sering Diperbarui oleh Program #

// ANTI-PATTERN: gunakan YAML sebagai database runtime
// YAML tidak mendukung penulisan parsial — seluruh file harus ditulis ulang
Future<void> tambahPengguna(String nama) async {
  final doc = loadYaml(await File('users.yaml').readAsString());
  // ✗ tidak efisien dan rentan race condition untuk data yang sering berubah
}

// BENAR: YAML untuk konfigurasi statis, database untuk data dinamis
// Gunakan SQLite, Hive, atau file JSON untuk data yang sering berubah

Indentasi Tidak Konsisten #

# ANTI-PATTERN: tab dan spasi tercampur
server:
  host: localhost
	port: 8080      # ← TAB — akan menyebabkan YamlException!

# BENAR: selalu gunakan spasi (bukan tab), konsisten 2 atau 4 spasi
server:
  host: localhost
  port: 8080

Ringkasan #

  • loadYaml mengembalikan YamlMap dan YamlList — bukan Map dan List Dart biasa. Keduanya read-only dan perlu dikonversi dengan fungsi helper yamlKeDart jika butuh modifikasi.
  • Konversi rekursif YamlMapMap<String, dynamic> diperlukan sebelum data YAML bisa digunakan sebagai argumen fungsi yang menerima Map Dart biasa.
  • Cast eksplisit saat mengakses nilai YAML — doc['port'] as int? — sama seperti Map<String, dynamic> dari JSON, karena nilainya bertipe dynamic.
  • loadYamlDocuments untuk file multi-dokumen yang dipisahkan --- — mengembalikan List<YamlDocument>.
  • Package yaml_writer untuk menulis YAML — package yaml sendiri hanya bisa membaca, bukan menulis.
  • Deep merge untuk konfigurasi multi-environment — base config di-merge dengan override per environment, nilai environment menimpa base.
  • YAML cocok untuk konfigurasi yang dibaca manusia, mendukung komentar, dan jarang berubah. Gunakan JSON untuk API dan data yang sering diperbarui secara programatik.
  • Jangan gunakan Tab dalam YAML — YAML hanya mengizinkan spasi untuk indentasi. Tab menyebabkan YamlException yang sulit di-debug.
  • Anchor (&) dan Alias (*) — package yaml Dart mendukung fitur ini untuk menghindari duplikasi dalam file YAML yang kompleks.
  • Error handling dengan on YamlException — lebih spesifik dari catch (e) generik dan memberikan informasi lokasi error (baris dan kolom) yang berguna untuk debugging.

← Sebelumnya: JSON   Berikutnya: MySQL →

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