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
}
Menulis YAML #
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 #
loadYamlmengembalikanYamlMapdanYamlList— bukanMapdanListDart biasa. Keduanya read-only dan perlu dikonversi dengan fungsi helperyamlKeDartjika butuh modifikasi.- Konversi rekursif
YamlMap→Map<String, dynamic>diperlukan sebelum data YAML bisa digunakan sebagai argumen fungsi yang menerimaMapDart biasa.- Cast eksplisit saat mengakses nilai YAML —
doc['port'] as int?— sama sepertiMap<String, dynamic>dari JSON, karena nilainya bertipedynamic.loadYamlDocumentsuntuk file multi-dokumen yang dipisahkan---— mengembalikanList<YamlDocument>.- Package
yaml_writeruntuk menulis YAML — packageyamlsendiri 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
YamlExceptionyang sulit di-debug.- Anchor (
&) dan Alias (*) — packageyamlDart mendukung fitur ini untuk menghindari duplikasi dalam file YAML yang kompleks.- Error handling dengan
on YamlException— lebih spesifik daricatch (e)generik dan memberikan informasi lokasi error (baris dan kolom) yang berguna untuk debugging.