YAML

YAML #

YAML (YAML Ain’t Markup Language) adalah format serialisasi data yang dirancang untuk mudah dibaca manusia. Dibandingkan JSON yang lebih ketat dengan tanda kurung dan kutip, YAML menggunakan indentasi dan tanda baca minimal — hasilnya jauh lebih bersih untuk file konfigurasi yang sering diedit manual. Ruby menggunakan YAML secara ekstensif: config/database.yml, config/locales/*.yml, config/credentials.yml.enc di Rails semuanya adalah YAML. Library yang digunakan Ruby adalah Psych — parser YAML yang ditulis oleh Aaron Patterson dan sudah bawaan sejak Ruby 1.9.3. Artikel ini membahas semua aspek YAML di Ruby, dari sintaks dasar hingga keamanan yang sering diabaikan.

Sintaks YAML — Panduan Cepat #

YAML menggunakan indentasi (spasi, bukan tab) untuk mendefinisikan struktur hierarki:

# Komentar dimulai dengan #

# Mapping (seperti Hash di Ruby)
nama: Rina Wijaya
umur: 28
kota: Bandung
aktif: true
saldo: 1500000.50

# Nilai null
email_sekunder: null   # atau: ~

# String multi-baris dengan | (literal, pertahankan newline)
bio: |
  Seorang developer Ruby
  yang senang dengan open source
  dan kopi hitam.  

# String multi-baris dengan > (folded, newline jadi spasi)
deskripsi: >
  Ini adalah teks panjang
  yang akan digabung menjadi
  satu baris dengan spasi.  

# Sequence (seperti Array di Ruby)
bahasa_pemrograman:
  - Ruby
  - Python
  - Go
  - Rust

# Sequence inline
hobi: [membaca, coding, hiking]

# Mapping inline
koordinat: {lat: -6.9147, lng: 107.6098}

# Nested mapping
alamat:
  jalan: Jl. Sudirman No. 42
  kelurahan: Dago
  kota: Bandung
  kode_pos: "40135"   # kutip untuk memaksa tipe String

# Array of mappings
riwayat_pekerjaan:
  - perusahaan: Startup ABC
    jabatan: Junior Developer
    tahun: 2019-2021
  - perusahaan: Tech Corp
    jabatan: Senior Developer
    tahun: 2021-sekarang

Tipe Data Otomatis di YAML #

YAML melakukan type coercion otomatis — ini bisa mengejutkan:

# Integer
jumlah: 42
negatif: -10
oktal: 0o17      # => 15
heks: 0xFF       # => 255

# Float
pi: 3.14159
notasi_e: 1.5e3  # => 1500.0
tak_terbatas: .inf
bukan_angka: .nan

# Boolean — HATI-HATI! Ini berbeda di YAML 1.1 vs 1.2
benar: true
salah: false
# Di YAML 1.1 (default Psych lama): yes, no, on, off juga boolean!
# Di YAML 1.2 (Psych 4+): hanya true dan false

# Tanggal dan waktu (otomatis jadi objek Ruby Time/Date!)
tanggal_lahir: 1990-08-17
timestamp: 2024-08-15 14:30:00 +07:00

# String yang mirip tipe lain — gunakan kutip
kode_pos: "40135"   # tanpa kutip → Integer
versi: "1.0"        # tanpa kutip → Float
aktif: "true"       # tanpa kutip → Boolean

Memuat YAML — Psych #

Psych adalah library YAML standar Ruby. Diakses melalui modul YAML:

require 'yaml'

# YAML.load — parse string YAML menjadi objek Ruby
yaml_str = <<~YAML
  nama: Budi
  umur: 30
  hobi:
    - membaca
    - coding
YAML

data = YAML.load(yaml_str)
puts data.class         # => Hash
puts data["nama"]       # => "Budi"
puts data["hobi"]       # => ["membaca", "coding"]

# YAML.load_file — parse langsung dari file
config = YAML.load_file("config/database.yml")
puts config["development"]["host"]

# Dengan permitted_classes untuk tipe yang diizinkan (Ruby 3.1+)
data = YAML.load(yaml_str, permitted_classes: [Symbol, Date, Time])

safe_load vs load — Keamanan Kritis #

# ANTI-PATTERN: YAML.load dengan input tidak terpercaya
# BERBAHAYA! Bisa mengeksekusi kode Ruby sembarang (deserialisasi object)
data = YAML.load(input_dari_user)   # CVE-2013-0156 — kerentanan Rails terkenal

# BENAR: YAML.safe_load — hanya parse tipe dasar
# Tidak akan deserialize objek Ruby kustom
data = YAML.safe_load(input_dari_user)

# safe_load secara default mengizinkan:
# String, Integer, Float, Array, Hash, true, false, nil

# Izinkan tipe tambahan secara eksplisit jika diperlukan
data = YAML.safe_load(
  yaml_str,
  permitted_classes: [Symbol, Date, Time, BigDecimal]
)

# Izinkan Symbol key (berguna untuk Rails fixtures)
data = YAML.safe_load(yaml_str, symbolize_names: true)
Aturan keamanan YAML:
  ✓ Gunakan safe_load untuk input dari pengguna atau sumber eksternal
  ✓ Gunakan safe_load untuk file konfigurasi yang bisa diedit pengguna
  ✓ Izinkan hanya kelas yang benar-benar dibutuhkan via permitted_classes
  ✗ Jangan gunakan load untuk input yang tidak kamu kontrol sepenuhnya
  ✗ Jangan deserialize objek kustom dari sumber tidak terpercaya

Mapping Tipe Data YAML ↔ Ruby #

YAML                    Ruby
────────────────────────────────────────────────
mapping {}          →   Hash (key: String secara default)
sequence []         →   Array
string              →   String
integer             →   Integer
float               →   Float
true / false        →   TrueClass / FalseClass
null / ~            →   NilClass
2024-08-15          →   Date (dengan safe_load + permitted_classes)
2024-08-15 14:30:00 →   Time (dengan safe_load + permitted_classes)
!!ruby/object:Kelas →   Instance kelas Ruby (HANYA dengan load, bukan safe_load)

Membuat YAML — dump dan dump_file #

YAML.dump mengonversi objek Ruby menjadi string YAML:

require 'yaml'

# Hash sederhana
data = { nama: "Citra", umur: 25, kota: "Surabaya" }
puts YAML.dump(data)
# => ---
# :nama: Citra
# :umur: 25
# :kota: Surabaya

# Perhatikan: Symbol key menjadi :nama, bukan nama
# Jika ingin String key, konversi dulu
data_str_keys = { "nama" => "Citra", "umur" => 25 }
puts YAML.dump(data_str_keys)
# => ---
# nama: Citra
# umur: 25

# Array
arr = ["Ruby", "Python", "Go"]
puts YAML.dump(arr)
# => ---
# - Ruby
# - Python
# - Go

# Struktur bersarang
config = {
  "development" => {
    "database" => "app_development",
    "host"     => "localhost",
    "port"     => 5432
  },
  "production" => {
    "database" => "app_production",
    "host"     => "db.example.com",
    "port"     => 5432
  }
}

puts YAML.dump(config)

# Simpan ke file
File.write("config/app.yml", YAML.dump(config))

# Atau menggunakan Psych langsung untuk kontrol lebih
File.open("config/app.yml", "w") do |f|
  f.write(YAML.dump(config))
end

Psych.dump dengan Opsi Indentasi #

# Kontrol indentasi output
yaml_output = Psych.dump(data, indentation: 4)   # 4 spasi
puts yaml_output

# Header "---" bisa dinonaktifkan
yaml_output = Psych.dump(data, header: false)

# Kumpulan lengkap opsi Psych.dump
Psych.dump(
  data,
  indentation:     2,      # spasi indentasi (default: 2)
  width:           80,     # lebar baris maksimal untuk folding
  header:          true,   # sertakan "---" di awal
  canonical:       false,  # format kanonik (lebih verbose)
  line_width:      80
)

Anchors dan Aliases — Hindari Duplikasi #

YAML mendukung anchors (&) dan aliases (*) untuk mendefinisikan nilai sekali dan merujuknya berkali-kali:

# Definisikan anchor
default_koneksi: &default_db
  adapter: postgresql
  encoding: unicode
  pool: 5
  timeout: 5000

# Alias — gunakan ulang nilai yang sama
development:
  <<: *default_db        # <<: merge semua key dari anchor
  database: app_development
  host: localhost

test:
  <<: *default_db
  database: app_test
  host: localhost

production:
  <<: *default_db
  database: app_production
  host: <%= ENV['DB_HOST'] %>
  pool: <%= ENV['DB_POOL'] || 10 %>

Ini adalah pola yang digunakan Rails di config/database.yml. <<: adalah merge key — semua pasangan key-value dari anchor digabungkan ke mapping saat ini, tapi bisa di-override:

require 'yaml'

yaml_str = <<~YAML
  default: &default
    timeout: 30
    retries: 3
    debug: false

  produksi:
    <<: *default
    timeout: 60      # override nilai default
    host: prod.db.com
YAML

config = YAML.safe_load(yaml_str)
puts config["produksi"]["timeout"]   # => 60  (override)
puts config["produksi"]["retries"]   # => 3   (dari anchor)
puts config["produksi"]["host"]      # => "prod.db.com"

Multi-Dokumen YAML #

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

# File dengan beberapa dokumen
---
id: 1
nama: Laptop
harga: 15000000
---
id: 2
nama: Mouse
harga: 350000
---
id: 3
nama: Keyboard
harga: 450000
require 'yaml'

# Baca semua dokumen dari file multi-dokumen
yaml_content = File.read("produk.yml")

# Psych.load_stream — parse semua dokumen
dokumen = []
Psych.load_stream(yaml_content) { |doc| dokumen << doc }
puts dokumen.length   # => 3
puts dokumen.first["nama"]   # => "Laptop"

# Atau dengan load_stream tanpa blok
dokumen = Psych.load_stream(yaml_content)
puts dokumen.inspect

# Buat file multi-dokumen
produk = [
  { id: 1, nama: "Laptop",   harga: 15_000_000 },
  { id: 2, nama: "Mouse",    harga:    350_000 },
  { id: 3, nama: "Keyboard", harga:    450_000 }
]

# Gabungkan dengan "---" sebagai separator
yaml_output = produk.map { |p| YAML.dump(p) }.join
File.write("produk.yml", yaml_output)

ERB di YAML — Konfigurasi Dinamis #

Rails dan banyak gem Ruby mendukung ERB (Embedded Ruby) di dalam file YAML. Ini memungkinkan nilai dinamis dari environment variables atau kalkulasi Ruby:

# config/database.yml — ERB di dalam YAML
default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV["DB_USERNAME"] || "postgres" %>
  password: <%= ENV["DB_PASSWORD"] %>
  host: <%= ENV["DB_HOST"] || "localhost" %>

development:
  <<: *default
  database: <%= "#{Rails.application.class.module_parent_name.underscore}_development" %>

test:
  <<: *default
  database: <%= "#{Rails.application.class.module_parent_name.underscore}_test" %>

production:
  <<: *default
  database: <%= ENV["DB_NAME"] %>
  host: <%= ENV["DB_HOST"] %>
  port: <%= ENV["DB_PORT"] || 5432 %>
# Parsing YAML dengan ERB secara manual
require 'yaml'
require 'erb'

def baca_yaml_dengan_erb(path)
  template = ERB.new(File.read(path))
  yaml_str = template.result(binding)
  YAML.safe_load(yaml_str)
end

config = baca_yaml_dengan_erb("config/app.yml")

Konfigurasi Rails dengan YAML #

YAML adalah tulang punggung konfigurasi di Rails. Berikut pola-pola umum:

Lokalisasi (i18n) #

# config/locales/id.yml
id:
  hello: "Halo"
  activerecord:
    models:
      user: "Pengguna"
      produk: "Produk"
    attributes:
      user:
        nama: "Nama Lengkap"
        email: "Alamat Email"
        umur: "Usia"
      produk:
        nama: "Nama Produk"
        harga: "Harga"
        stok: "Stok"
    errors:
      models:
        user:
          attributes:
            email:
              blank: "tidak boleh kosong"
              invalid: "format tidak valid"
            nama:
              too_short: "minimal %{count} karakter"

Konfigurasi Aplikasi Kustom #

# config/app_config.yml
defaults: &defaults
  nama_aplikasi: "Toko Online Ku"
  versi: "2.1.0"
  max_upload_size: 10485760   # 10 MB dalam byte
  format_gambar_izin:
    - jpg
    - jpeg
    - png
    - webp
  email_admin: "[email protected]"
  fitur:
    live_chat: true
    payment_gateway: midtrans
    review_produk: true

development:
  <<: *defaults
  debug_mode: true
  email_admin: "dev@localhost"
  fitur:
    live_chat: false   # nonaktifkan di development

test:
  <<: *defaults
  debug_mode: false

production:
  <<: *defaults
  debug_mode: false
  max_upload_size: 5242880   # lebih ketat di produksi: 5 MB
# Muat konfigurasi dengan ERB dan environment detection
class AppConfig
  def self.muat
    path = Rails.root.join("config", "app_config.yml")
    raw  = ERB.new(File.read(path)).result
    semua = YAML.safe_load(raw, permitted_classes: [Symbol])

    # Ambil konfigurasi untuk environment saat ini
    defaults = semua["defaults"] || {}
    env_config = semua[Rails.env] || {}
    defaults.merge(env_config)
  end

  CONFIG = muat.freeze

  def self.[](kunci)
    CONFIG[kunci.to_s]
  end
end

# Akses
puts AppConfig["nama_aplikasi"]    # => "Toko Online Ku"
puts AppConfig["max_upload_size"]  # => 5242880 (di production)

Fixtures Rails — YAML untuk Test Data #

Rails menggunakan YAML untuk fixtures — data test yang dimuat ke database sebelum test:

# test/fixtures/users.yml
rina:
  nama: Rina Wijaya
  email: [email protected]
  role: user
  aktif: true
  created_at: <%= Time.now.utc %>

admin:
  nama: Admin Utama
  email: [email protected]
  role: admin
  aktif: true
  created_at: <%= Time.now.utc %>

user_nonaktif:
  nama: User Lama
  email: [email protected]
  role: user
  aktif: false
  created_at: 2020-01-01 00:00:00
# Penggunaan di test MiniTest
class UserTest < ActiveSupport::TestCase
  test "user aktif bisa login" do
    user = users(:rina)   # ambil fixture berdasarkan label
    assert user.aktif?
  end

  test "admin punya akses penuh" do
    admin = users(:admin)
    assert admin.role == "admin"
  end
end

Serialisasi Objek Kustom #

YAML bisa menyimpan dan memuat kembali objek Ruby kustom — tapi ini harus dilakukan dengan hati-hati karena alasan keamanan:

require 'yaml'

class Titik
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  def to_s
    "(#{@x}, #{@y})"
  end

  # Kustomisasi representasi YAML (opsional)
  def encode_with(coder)
    coder["x"] = @x
    coder["y"] = @y
  end

  def init_with(coder)
    @x = coder["x"]
    @y = coder["y"]
  end
end

titik = Titik.new(3, 4)

# Serialize ke YAML
yaml_str = YAML.dump(titik)
puts yaml_str
# => --- !ruby/object:Titik
#    x: 3
#    y: 4

# Deserialize — HANYA gunakan YAML.load, BUKAN safe_load
titik_kembali = YAML.load(yaml_str, permitted_classes: [Titik])
puts titik_kembali   # => (3, 4)
puts titik_kembali.x # => 3
Serialisasi objek Ruby kustom ke YAML hanya aman jika kamu mengontrol sepenuhnya file YAML yang dimuat. Jangan pernah memuat YAML dari input pengguna atau jaringan dengan YAML.load — gunakan selalu YAML.safe_load dengan permitted_classes yang minimal.

Membaca dan Menulis File YAML #

require 'yaml'

# Baca file YAML
def baca_yaml(path)
  YAML.safe_load_file(path, symbolize_names: true)
rescue Errno::ENOENT
  raise "File tidak ditemukan: #{path}"
rescue Psych::SyntaxError => e
  raise "Sintaks YAML tidak valid di #{path}: #{e.message}"
end

config = baca_yaml("config/settings.yml")
puts config[:database][:host]

# Tulis ke file YAML
def tulis_yaml(path, data)
  File.write(path, YAML.dump(data))
rescue IOError => e
  raise "Gagal menulis file: #{e.message}"
end

tulis_yaml("output/hasil.yml", { status: "sukses", total: 42 })

# Perbarui file YAML yang sudah ada
def perbarui_yaml(path, &blok)
  data = baca_yaml(path)
  data_baru = blok.call(data)
  tulis_yaml(path, data_baru.transform_keys(&:to_s))
end

perbarui_yaml("config/settings.yml") do |config|
  config.merge(versi: "2.0", diperbarui: Time.now.to_s)
end

# Baca YAML dengan ERB
def baca_yaml_erb(path, binding_obj = binding)
  template = ERB.new(File.read(path))
  yaml_str = template.result(binding_obj)
  YAML.safe_load(yaml_str, permitted_classes: [Symbol, Date, Time])
end

Penanganan Error YAML #

# Error sintaks YAML
begin
  YAML.safe_load("nama: Rina\n  bad_indent")
rescue Psych::SyntaxError => e
  puts "Sintaks tidak valid: #{e.message}"
  puts "Baris: #{e.line}, Kolom: #{e.column}"
end

# Error parsing karena kelas tidak diizinkan
begin
  YAML.safe_load("--- !ruby/object:Titik\nx: 3")
rescue Psych::DisallowedClass => e
  puts "Kelas tidak diizinkan: #{e.message}"
end

# Validasi sebelum load
def yaml_valid?(str)
  YAML.safe_load(str)
  true
rescue Psych::SyntaxError
  false
end

puts yaml_valid?("nama: Rina")    # => true
puts yaml_valid?("nama: : oops")  # => false

Perbandingan YAML vs JSON #

AspekYAMLJSON
KeterbacaanSangat tinggi — tanpa kurung, kutip minimalSedang — banyak tanda baca
Penulisan manualLebih mudah diedit manusiaLebih mudah salah (kutip terlupa)
KomentarDidukung (#)Tidak didukung
Tipe dataLebih kaya (Date, Time, dll.)Terbatas (string, number, bool, null)
KeamananHarus hati-hati (object injection)Lebih aman secara default
Performa parsingLebih lambatLebih cepat
Dukungan toolingLebih sedikitUniversal
Cocok untukFile konfigurasi, fixtures, i18nAPI response, data exchange
# Konversi YAML ↔ JSON
require 'yaml'
require 'json'

# YAML → JSON
yaml_str = File.read("config.yml")
data = YAML.safe_load(yaml_str)
json_str = JSON.pretty_generate(data)
File.write("config.json", json_str)

# JSON → YAML
json_str = File.read("data.json")
data = JSON.parse(json_str)
yaml_str = YAML.dump(data)
File.write("data.yml", yaml_str)

Anti-Pattern YAML yang Harus Dihindari #

# ANTI-PATTERN 1: Nilai tanpa kutip yang ambigu
kode_pos: 40135      # → Integer!  bukan String
versi: 1.0           # → Float! bukan String
aktif: yes           # → true di YAML 1.1 (Psych lama)

# BENAR: kutip untuk nilai yang harus jadi String
kode_pos: "40135"
versi: "1.0"
aktif: "yes"         # atau gunakan boolean true/false yang eksplisit

# ANTI-PATTERN 2: Tab untuk indentasi
# YAML tidak mengizinkan tab — harus spasi!
database:
	host: localhost    # ← tab — SyntaxError!

# BENAR: spasi
database:
  host: localhost    # ← 2 spasi

# ANTI-PATTERN 3: Kunci duplikat
server:
  host: localhost
  port: 5432
  host: db.example.com   # duplikat! nilai pertama diabaikan

# BENAR: setiap kunci unik
server:
  host: db.example.com
  port: 5432
# ANTI-PATTERN 4: YAML.load dengan input tidak terpercaya
config = YAML.load(params[:config])   # ← berbahaya! bisa eksekusi kode

# BENAR: safe_load
config = YAML.safe_load(params[:config])

# ANTI-PATTERN 5: Menyimpan kredensial langsung di YAML yang di-commit
# config/database.yml
production:
  password: "p@ssw0rd123secret"   # ← jangan commit ini!

# BENAR: gunakan environment variable atau Rails credentials
production:
  password: <%= ENV["DB_PASSWORD"] %>

Ringkasan #

  • Gunakan safe_load bukan loadYAML.load bisa mengeksekusi kode Ruby sewenang-wenang dari input tidak terpercaya; safe_load hanya mengizinkan tipe dasar dan aman untuk hampir semua kasus.
  • Kutip nilai yang ambigu"40135", "1.0", "true" untuk memastikan nilainya String, bukan Integer/Float/Boolean yang mungkin tidak diharapkan.
  • Spasi, bukan tab — YAML hanya mengizinkan spasi untuk indentasi; tab menyebabkan Psych::SyntaxError yang membingungkan.
  • Anchors dan aliases untuk DRY&anchor dan *alias dengan <<: untuk merge menghindari duplikasi konfigurasi antar environment.
  • ERB di YAML untuk nilai dinamis — gunakan <%= ENV["VAR"] %> untuk konfigurasi yang berbeda per environment tanpa hardcode.
  • permitted_classes untuk tipe non-standar — jika butuh Date atau Time dari YAML, tambahkan ke permitted_classes: [Date, Time] di safe_load.
  • Jangan commit kredensial di YAML — gunakan environment variable atau Rails credentials yang terenkripsi, bukan plaintext di file yang di-commit ke Git.
  • Psych::SyntaxError untuk deteksi format salah — tangani exception ini saat membaca YAML dari sumber eksternal atau file yang bisa diedit pengguna.
  • YAML untuk konfigurasi, JSON untuk API — YAML lebih baik untuk file yang sering diedit manusia (konfigurasi, i18n, fixtures); JSON lebih baik untuk pertukaran data antar sistem.
  • Multi-dokumen dengan Psych.load_stream — satu file YAML bisa berisi beberapa dokumen yang dipisahkan ---; berguna untuk seed data atau batch import.

← Sebelumnya: JSON   Berikutnya: MySQL →

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