YAML #

YAML (YAML Ain’t Markup Language) is a data serialization format designed for human readability — using indentation and minimal symbols compared to JSON’s brackets and quotes. In the Dart ecosystem, YAML is used everywhere: pubspec.yaml, analysis_options.yaml, build.yaml, and application configuration files. The yaml package provides a parser for reading YAML, while writing YAML can be done with yaml_writer. This article covers reading and writing YAML effectively, converting to model classes, and when YAML is more appropriate than JSON.

YAML Syntax — Quick Reference #

Before diving into Dart code, it’s important to understand the YAML syntax you’ll be parsing:

# Comments start with #

# Scalar — primitive values
nama: Budi Santoso
umur: 25
tinggi: 1.75
aktif: true
kosong: null            # or ~

# Strings — quotes optional, required if there are special characters
kota: Jakarta
alamat: "Jl. Merdeka No. 1, Jakarta"
pesan: 'This has "quotes" in the string'

# Multi-line strings
deskripsi: |
  This is the first line.
  This is the second line.
  Newlines are preserved.  

ringkasan: >
  This is a long paragraph that
  gets wrapped. Newlines become spaces,
  except for new paragraphs.  

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

# Inline list
warna: [merah, hijau, biru]

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

# Inline map
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 (&) and Alias (*) — reuse values
default_db: &db_default
  host: localhost
  port: 5432

produksi:
  <<: *db_default       # merge from the anchor
  host: prod.example.com  # override specific fields
  nama: prod_db

# Multi-document in one file (separated by ---)
---
dokumen: pertama
---
dokumen: kedua

Setting Up the yaml Package #

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

loadYaml — Basic Parsing #

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);

  // The loadYaml result is a YamlMap — like a Map but not a regular Dart Map
  print(doc.runtimeType);    // YamlMap

  // Access values — similar to a 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 because it's quoted in YAML)
}

YamlMap vs Dart Map #

loadYaml returns YamlMap and YamlList, not regular Dart Map and List. Both are read-only — they can’t be modified. For use cases needing regular Map/List, convert first:

import 'package:yaml/yaml.dart';

// Recursive conversion of YamlMap/YamlList to native Dart types
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>;

  // Now it can be modified
  dartMap['email'] = '[email protected]';
  print(dartMap); // {nama: Budi, umur: 25, email: [email protected]}
}

Reading YAML from a 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('YAML file not found', 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 into Model Classes #

The same pattern as JSON — create a fromYaml factory constructor:

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,
    );
  }
}

// Usage
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('Features: ${config.fiturAktif.join(', ')}');
}

Multi-Document YAML #

One YAML file can contain several documents separated by ---:

import 'package:yaml/yaml.dart';

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

  // loadYamlDocuments — parse all documents at once
  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
}

Writing YAML #

The yaml package is read-only. To write YAML, use 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

Saving to a 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('Configuration saved to $path');
}

YAML vs JSON vs TOML — When to Use Each #

flowchart TD
    A{Data format needs?} --> B{Read/edited\\ndirectly by humans?}
    B -- No --> C[JSON\\nfor APIs and data transfer]
    B -- Yes --> D{Comments needed?}
    D -- No --> E{Simple\\nstructure?}
    E -- Yes --> F[JSON or YAML]
    E -- No --> G[YAML\\nfor complex configuration]
    D -- Yes --> H{Syntax preference?}
    H -- Indentation --> G
    H -- Key = value --> I[TOML\\nfor simple configuration]
AspectYAMLJSONTOML
Human readability✓✓✓ Excellent✓✓ Good✓✓✓ Excellent
Comments✓ Yes (#)✗ No✓ Yes (#)
Data typesRich (date, binary)LimitedRich (date, array)
VerboseMinimalModerateMinimal
Error proneHigh (whitespace)LowLow
Best forComplex config, CI/CDAPIs, data transferSimple config
Dart supportyaml packageBuilt-in (dart:convert)toml package
// The same thing in three formats:

// 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: Structured Application Configuration #

YAML is very common for multi-environment configuration:

# config/base.yaml — base configuration
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 — overrides for 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';

// Multi-environment configuration loader
Future<Map<String, dynamic>> muatKonfigurasi(String lingkungan) async {
  // Load the base config
  final base = await _bacaYaml('config/base.yaml');

  // Load the 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 overrides 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 of two Maps — right values override left, nested Maps are merged
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;
}

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

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

YAML Anti-Patterns #

Access Without Type Validation #

// ANTI-PATTERN: assuming types without casts or validation
final doc = loadYaml(yamlString);
final port = doc['server']['port'] * 2; // ✗ dynamic — can crash

// CORRECT: explicit casts with fallbacks
final port = (doc['server']['port'] as int?) ?? 8080;
final portDikalikan = port * 2;

YAML for Frequently Program-Updated Data #

// ANTI-PATTERN: using YAML as a runtime database
// YAML doesn't support partial writes — the whole file must be rewritten
Future<void> tambahPengguna(String nama) async {
  final doc = loadYaml(await File('users.yaml').readAsString());
  // ✗ inefficient and prone to race conditions for frequently changing data
}

// CORRECT: YAML for static configuration, a database for dynamic data
// Use SQLite, Hive, or a JSON file for frequently changing data

Inconsistent Indentation #

# ANTI-PATTERN: mixing tabs and spaces
server:
  host: localhost
	port: 8080      # ← TAB — will cause a YamlException!

# CORRECT: always use spaces (not tabs), consistently 2 or 4 spaces
server:
  host: localhost
  port: 8080

Summary #

  • loadYaml returns YamlMap and YamlList — not regular Dart Map and List. Both are read-only and need conversion with a yamlKeDart helper function if you need to modify them.
  • Recursive conversion of YamlMapMap<String, dynamic> is needed before YAML data can be used as an argument to functions accepting regular Dart Maps.
  • Explicit casts when accessing YAML values — doc['port'] as int? — just like Map<String, dynamic> from JSON, because the values are typed dynamic.
  • loadYamlDocuments for multi-document files separated by --- — returns a List<YamlDocument>.
  • The yaml_writer package for writing YAML — the yaml package itself can only read, not write.
  • Deep merge for multi-environment configuration — the base config is merged with per-environment overrides, environment values overriding the base.
  • YAML is good for configuration that humans read, supports comments, and rarely changes. Use JSON for APIs and data frequently updated programmatically.
  • Don’t use Tabs in YAML — YAML only allows spaces for indentation. Tabs cause hard-to-debug YamlExceptions.
  • Anchors (&) and Aliases (*) — the Dart yaml package supports these features to avoid duplication in complex YAML files.
  • Error handling with on YamlException — more specific than a generic catch (e) and provides useful error location info (line and column) for debugging.

← Previous: JSON   Next: MySQL →

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