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 denganYAML.load— gunakan selaluYAML.safe_loaddenganpermitted_classesyang 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 #
| Aspek | YAML | JSON |
|---|---|---|
| Keterbacaan | Sangat tinggi — tanpa kurung, kutip minimal | Sedang — banyak tanda baca |
| Penulisan manual | Lebih mudah diedit manusia | Lebih mudah salah (kutip terlupa) |
| Komentar | Didukung (#) | Tidak didukung |
| Tipe data | Lebih kaya (Date, Time, dll.) | Terbatas (string, number, bool, null) |
| Keamanan | Harus hati-hati (object injection) | Lebih aman secara default |
| Performa parsing | Lebih lambat | Lebih cepat |
| Dukungan tooling | Lebih sedikit | Universal |
| Cocok untuk | File konfigurasi, fixtures, i18n | API 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_loadbukanload—YAML.loadbisa mengeksekusi kode Ruby sewenang-wenang dari input tidak terpercaya;safe_loadhanya 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::SyntaxErroryang membingungkan.- Anchors dan aliases untuk DRY —
&anchordan*aliasdengan<<: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_classesuntuk tipe non-standar — jika butuh Date atau Time dari YAML, tambahkan kepermitted_classes: [Date, Time]disafe_load.- Jangan commit kredensial di YAML — gunakan environment variable atau Rails credentials yang terenkripsi, bukan plaintext di file yang di-commit ke Git.
Psych::SyntaxErroruntuk 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.