YAML #
YAML (YAML Ain’t Markup Language) is a data serialization format designed to be easy for humans to read. It dominates the configuration world — Docker Compose, Kubernetes manifests, GitHub Actions, Ansible playbooks, and many more all use YAML. In Rust, YAML support is provided by the serde_yaml crate, which works exactly like serde_json: it uses the Serialize and Deserialize traits from serde, so structs already working with JSON can be used for YAML immediately without modification. This article covers reading and writing YAML, format-control annotations, dynamic YAML Value, common config file patterns, and comparisons with JSON and TOML.
Installation #
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9"
Basic Serialization and Deserialization #
Struct to YAML #
use serde::{Deserialize, Serialize};
use serde_yaml;
#[derive(Serialize, Deserialize, Debug)]
struct KonfigServer {
host: String,
port: u16,
workers: u32,
debug: bool,
database_url: String,
}
fn main() {
let config = KonfigServer {
host: String::from("0.0.0.0"),
port: 8080,
workers: 4,
debug: false,
database_url: String::from("postgres://localhost/produksi"),
};
// Serialize to a YAML string
let yaml_str = serde_yaml::to_string(&config).unwrap();
println!("{}", yaml_str);
// host: 0.0.0.0
// port: 8080
// workers: 4
// debug: false
// database_url: postgres://localhost/produksi
// Serialize to a writer (file, stdout, etc.)
let mut output = Vec::new();
serde_yaml::to_writer(&mut output, &config).unwrap();
println!("YAML bytes: {}", output.len());
}
YAML to Struct #
use serde::Deserialize;
use serde_yaml;
#[derive(Deserialize, Debug)]
struct Aplikasi {
nama: String,
versi: String,
port: u16,
fitur: Vec<String>,
batas: Batas,
}
#[derive(Deserialize, Debug)]
struct Batas {
maks_koneksi: u32,
timeout_detik: u64,
maks_body_mb: f64,
}
fn main() {
let yaml_str = "
nama: Aplikasi Rust
versi: '2.1.0'
port: 3000
fitur:
- auth
- caching
- metrics
batas:
maks_koneksi: 100
timeout_detik: 30
maks_body_mb: 10.5
";
// Deserialize from &str
let app: Aplikasi = serde_yaml::from_str(yaml_str).unwrap();
println!("Name: {}", app.nama);
println!("Features: {:?}", app.fitur);
println!("Max connections: {}", app.batas.maks_koneksi);
// Deserialize from a file
let file = std::fs::File::open("config.yaml");
if let Ok(f) = file {
let config: Aplikasi = serde_yaml::from_reader(f).unwrap();
println!("From file: {}", config.nama);
}
// Informative error handling
let yaml_rusak = "port: bukan_angka";
match serde_yaml::from_str::<Aplikasi>(yaml_rusak) {
Ok(_) => println!("Success"),
Err(e) => println!("Error: {}", e),
}
}
A Realistic Configuration File #
YAML is most often used for configuration files. Here’s a pattern commonly used in production applications:
use serde::{Deserialize, Serialize};
use std::fs;
#[derive(Debug, Deserialize, Serialize)]
struct KonfigLengkap {
aplikasi: KonfigAplikasi,
database: KonfigDatabase,
redis: KonfigRedis,
log: KonfigLog,
keamanan: KonfigKeamanan,
}
#[derive(Debug, Deserialize, Serialize)]
struct KonfigAplikasi {
nama: String,
host: String,
port: u16,
#[serde(default)]
debug: bool,
#[serde(default = "versi_default")]
versi: String,
}
fn versi_default() -> String {
String::from("0.1.0")
}
#[derive(Debug, Deserialize, Serialize)]
struct KonfigDatabase {
url: String,
maks_koneksi: u32,
#[serde(default = "timeout_default")]
timeout_detik: u64,
ssl: bool,
}
fn timeout_default() -> u64 { 30 }
#[derive(Debug, Deserialize, Serialize)]
struct KonfigRedis {
url: String,
ttl_detik: u64,
}
#[derive(Debug, Deserialize, Serialize)]
struct KonfigLog {
level: String,
format: String,
file: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
struct KonfigKeamanan {
jwt_rahasia: String,
bcrypt_cost: u8,
cors_origins: Vec<String>,
}
fn muat_konfigurasi(path: &str) -> Result<KonfigLengkap, Box<dyn std::error::Error>> {
let isi = fs::read_to_string(path)?;
let config: KonfigLengkap = serde_yaml::from_str(&isi)?;
Ok(config)
}
fn main() {
// Example YAML matching the struct above
let yaml_contoh = r#"
aplikasi:
nama: "API Server"
host: "0.0.0.0"
port: 8080
debug: true
database:
url: "postgres://user:***@localhost:5432/db"
maks_koneksi: 20
ssl: true
redis:
url: "redis://localhost:6379"
ttl_detik: 3600
log:
level: "info"
format: "json"
file: "/var/log/app.log"
keamanan:
jwt_rahasia: "rahasia-super-panjang"
bcrypt_cost: 12
cors_origins:
- "https://app.contoh.com"
- "https://admin.contoh.com"
"#;
let config: KonfigLengkap = serde_yaml::from_str(yaml_contoh).unwrap();
println!("Server: {}:{}", config.aplikasi.host, config.aplikasi.port);
println!("DB: {}", config.database.url);
println!("CORS: {:?}", config.keamanan.cors_origins);
// Write the config back to YAML
let yaml_kembali = serde_yaml::to_string(&config).unwrap();
println!("\n--- Configuration as YAML ---\n{}", yaml_kembali);
}
#[serde(...)] Annotations for YAML
#
Every serde annotation that works for JSON also works for YAML — they’re part of the serde framework, not specific to any format:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")] // Rust snake_case → YAML kebab-case
struct KonfigDocker {
image_name: String, // → image-name
container_port: u16, // → container-port
restart_policy: String, // → restart-policy
environment_vars: Vec<String>, // → environment-vars
}
#[derive(Serialize, Deserialize, Debug)]
struct Layanan {
nama: String,
aktif: bool,
// Use the default if absent in YAML
#[serde(default)]
replika: u32,
// Not included if None
#[serde(skip_serializing_if = "Option::is_none")]
catatan: Option<String>,
// Specific rename
#[serde(rename = "healthcheck-url")]
url_healthcheck: String,
}
fn main() {
let layanan = Layanan {
nama: String::from("api-server"),
aktif: true,
replika: 3,
catatan: None,
url_healthcheck: String::from("/health"),
};
let yaml = serde_yaml::to_string(&layanan).unwrap();
println!("{}", yaml);
// nama: api-server
// aktif: true
// replika: 3
// healthcheck-url: /health
// (catatan doesn't appear because it's None)
// Deserialize with missing fields using defaults
let yaml_minimal = "
nama: worker
aktif: false
healthcheck-url: /ping
";
let layanan2: Layanan = serde_yaml::from_str(yaml_minimal).unwrap();
println!("Default replica: {}", layanan2.replika); // 0
}
Dynamic YAML Value #
Like serde_json::Value, serde_yaml provides serde_yaml::Value for YAML whose structure isn’t known at compile time:
use serde_yaml::Value;
fn main() {
let yaml_str = "
server:
host: localhost
port: 8080
tags:
- web
- api
database:
primary:
url: postgres://localhost/db
pool: 10
replica:
url: postgres://replica/db
pool: 5
";
let nilai: Value = serde_yaml::from_str(yaml_str).unwrap();
// Access fields with indexing (same as serde_json::Value)
println!("Host: {}", nilai["server"]["host"]);
println!("Port: {}", nilai["server"]["port"]);
// Iterate a sequence (array)
if let Some(tags) = nilai["server"]["tags"].as_sequence() {
println!("Tags:");
for tag in tags {
println!(" - {}", tag.as_str().unwrap_or(""));
}
}
// Iterate a mapping (object)
if let Some(db) = nilai["database"].as_mapping() {
for (kunci, val) in db {
println!("DB {}: pool={}", kunci.as_str().unwrap_or("?"),
val["pool"].as_i64().unwrap_or(0));
}
}
// Build YAML from a Value programmatically
let mut map = serde_yaml::Mapping::new();
map.insert(Value::String("host".into()), Value::String("localhost".into()));
map.insert(Value::String("port".into()), Value::Number(3000.into()));
let mut seq = serde_yaml::Sequence::new();
seq.push(Value::String("fitur-a".into()));
seq.push(Value::String("fitur-b".into()));
map.insert(Value::String("fitur".into()), Value::Sequence(seq));
let nilai_baru = Value::Mapping(map);
println!("{}", serde_yaml::to_string(&nilai_baru).unwrap());
}
YAML Features That JSON Doesn’t Have #
Multi-Document in One File #
YAML supports multiple documents in a single file, separated by ---:
use serde::Deserialize;
use serde_yaml;
#[derive(Deserialize, Debug)]
struct Sumber {
nama: String,
tipe: String,
}
fn main() {
let yaml_multi = "
---
nama: database-primary
tipe: postgresql
---
nama: cache-server
tipe: redis
---
nama: message-broker
tipe: rabbitmq
";
// Deserialize multi-document
let iter = serde_yaml::Deserializer::from_str(yaml_multi);
for dokumen in iter {
let sumber = Sumber::deserialize(dokumen).unwrap();
println!("{}: {}", sumber.nama, sumber.tipe);
}
}
Comments in YAML #
This is an important feature that JSON doesn’t have — YAML supports comments with #. Very useful for configuration files:
# Main configuration file
# Created: 2024-08-24
server:
host: 0.0.0.0 # Listen on all interfaces
port: 8080 # Default port, change if there's a conflict
database:
# Format: postgres://user:***@host:port/dbname
url: postgres://admin:***@localhost:5432/produksi
maks_koneksi: 20 # Adjust to the server specifications
Comments are ignored when parsing — they don’t appear when YAML is serialized back.
Anchors and Aliases — Avoiding Duplication #
YAML has an anchor (&nama) and alias (*nama) mechanism for reusing values:
# Define a base config as an anchor
default_db: &default_db
maks_koneksi: 10
timeout: 30
ssl: true
database:
primary:
<<: *default_db # merge key — copies all fields from the anchor
url: postgres://primary/db
maks_koneksi: 20 # overrides the merged field
replica:
<<: *default_db # reuse the default config
url: postgres://replica/db
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct DbConfig {
url: String,
maks_koneksi: u32,
timeout: u32,
ssl: bool,
}
#[derive(Deserialize, Debug)]
struct Database {
primary: DbConfig,
replica: DbConfig,
}
fn main() {
let yaml = "
default_db: &default_db
maks_koneksi: 10
timeout: 30
ssl: true
database:
primary:
<<: *default_db
url: postgres://primary/db
maks_koneksi: 20
replica:
<<: *default_db
url: postgres://replica/db
";
// serde_yaml handles anchors/aliases automatically
let nilai: serde_yaml::Value = serde_yaml::from_str(yaml).unwrap();
println!("Primary maks_koneksi: {}", nilai["database"]["primary"]["maks_koneksi"]);
println!("Replica maks_koneksi: {}", nilai["database"]["replica"]["maks_koneksi"]);
println!("Primary ssl: {}", nilai["database"]["primary"]["ssl"]);
}
Embedding Config Files with include_str!
#
To include a YAML file as part of the binary (embedding), use the include_str! macro:
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct KonfigDefault {
versi: String,
fitur_aktif: Vec<String>,
batas_rate: u32,
}
// The config file is embedded into the binary at compile time
const KONFIGURASI_DEFAULT: &str = include_str!("../config/default.yaml");
fn main() {
let config: KonfigDefault = serde_yaml::from_str(KONFIGURASI_DEFAULT).unwrap();
println!("Default version: {}", config.versi);
println!("Features: {:?}", config.fitur_aktif);
// Common pattern: default config from the binary, override from an external file
let config_override = std::fs::read_to_string("config.local.yaml")
.unwrap_or_default();
if !config_override.is_empty() {
let local: KonfigDefault = serde_yaml::from_str(&config_override).unwrap();
println!("Local override: {}", local.versi);
}
}
YAML ↔ JSON Conversion #
Since both use serde, converting between YAML and JSON is very easy:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct Konfigurasi {
nama: String,
port: u16,
fitur: Vec<String>,
}
fn yaml_ke_json(yaml_str: &str) -> Result<String, Box<dyn std::error::Error>> {
// Parse YAML into a Value
let nilai: serde_yaml::Value = serde_yaml::from_str(yaml_str)?;
// Convert to serde_json::Value via serde
let json_nilai: serde_json::Value = serde_json::to_value(nilai)?;
// Serialize to a JSON string
Ok(serde_json::to_string_pretty(&json_nilai)?)
}
fn json_ke_yaml(json_str: &str) -> Result<String, Box<dyn std::error::Error>> {
let nilai: serde_json::Value = serde_json::from_str(json_str)?;
let yaml_nilai: serde_yaml::Value = serde_yaml::to_value(nilai)?;
Ok(serde_yaml::to_string(&yaml_nilai)?)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let yaml_str = "
nama: Server Produksi
port: 443
fitur:
- tls
- caching
- compression
";
let json_hasil = yaml_ke_json(yaml_str)?;
println!("JSON:\n{}", json_hasil);
let json_str = r#"{"nama": "Dev Server", "port": 3000, "fitur": ["debug"]}"#;
let yaml_hasil = json_ke_yaml(json_str)?;
println!("YAML:\n{}", yaml_hasil);
// Or more directly: roundtrip via a typed struct
let config: Konfigurasi = serde_yaml::from_str(yaml_str).unwrap();
let sebagai_json = serde_json::to_string_pretty(&config).unwrap();
println!("Via struct:\n{}", sebagai_json);
Ok(())
}
YAML vs JSON vs TOML — When to Choose #
| Aspect | YAML | JSON | TOML |
|---|---|---|---|
| Readability | Very high | Moderate | High |
| Comments | Yes (#) | No | Yes (#) |
| Verbosity | Low | Moderate | Moderate |
| Parser complexity | High | Low | Moderate |
| Multi-document | Yes (---) | No | No |
| Anchors/aliases | Yes | No | No |
| Error on wrong indentation | Yes | No | No |
| Best for | Complex configs, DevOps | APIs, data exchange | Cargo.toml, app configs |
Use YAML if:
✓ Config files often edited by humans (Docker, K8s, CI/CD)
✓ Need comments to explain values
✓ Deep hierarchical configuration
✓ Need anchors/aliases to avoid duplication
Use JSON if:
✓ API responses/requests — the de facto standard
✓ Data exchange between systems
✓ No comments needed
✓ Faster, simpler parsers
Use TOML if:
✓ Rust application configs (Cargo.toml, rust-toolchain.toml)
✓ Simple to medium config files
✓ Developers are more familiar with INI-like formats
Summary #
serde_yamluses the same serde traits asserde_json— structs with#[derive(Serialize, Deserialize)]work immediately without modification.from_strfor strings,from_readerfor files —from_readeris more efficient for large files because it streams.- All
#[serde(...)]annotations work —rename,rename_all,skip_serializing_if,default,skipare all valid for YAML just like for JSON.serde_yaml::Valuefor dynamic YAML — access via["kunci"],.as_sequence(),.as_mapping(),.as_str(),.as_i64().- Multi-document with
serde_yaml::Deserializer::from_str— parse several YAML documents separated by---in one string.- Anchors and aliases (
&namaand*nama) are handled automatically byserde_yaml— you can use them in YAML files without extra code.include_str!to embed a YAML file into the binary at compile time — suitable for default configs that must not be lost.- YAML ↔ JSON conversion is easy — via a typed struct (
from_yaml→to_json) or via an intermediate Value.- YAML is better than JSON for config files because it supports comments, is easier to read, and has fewer boilerplate characters (no quotes for keys, no commas).