Skip to Content
Course content

167: Serializing to TOML and YAML

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was working on a project where we used a JSON file for the application's main configuration. It seemed fine at first, but we quickly hit a wall. A DevOps engineer tried to add a new environment variable to the config, missed a single comma on the line above, and accidentally brought down the entire staging environment because the parser choked. We spent an hour debugging a syntax error that should have been trivial. That was the day I realized that while JSON is king for machine-to-machine communication, it's a nightmare for humans to edit. We migrated our config to TOML and our deployment manifests to YAML immediately.

Since you've already mastered the basics of serde, you know that the real power of the ecosystem is that the serialization logic is decoupled from the data format. To move from JSON to TOML or YAML, you don't change your structs; you just change the crate that handles the string conversion. It's a seamless swap.

Structuring Configuration with TOML

TOML (Tom's Obvious, Minimal Language) is essentially the gold standard for Rust configuration—after all, it's what Cargo.toml uses. It's designed to be mapped directly to hash tables, which makes it a perfect fit for Rust structs. When you're building a CLI tool or a long-running service, I highly recommend TOML for the settings file because it handles key-value pairs and sections much more cleanly than JSON.

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct ServerConfig {
    host: String,
    port: u16,
    timeout: Option<u32>,
    [serde(rename = "database_settings")]
    db: DatabaseConfig,
}

#[derive(Serialize, Deserialize, Debug)]
struct DatabaseConfig {
    connection_limit: u32,
    retry_on_failure: bool,
}

fn main() {
    let config = ServerConfig {
        host: "127.0.0.1".to_string(),
        port: 8080,
        timeout: Some(30),
        db: DatabaseConfig {
            connection_limit: 100,
            retry_on_failure: true,
        },
    };

    // Serializing to TOML string
    let toml_string = toml::to_string(&config).expect("Failed to serialize to TOML");
    println!("TOML Output:\n{}", toml_string);
}

Notice how the [serde(rename = "...")] attribute allows us to keep our Rust field names idiomatic (using db) while keeping the config file descriptive for the user. I've found that this prevents a lot of friction between the developers and the people actually managing the production config files.

Handling Complex Data with YAML

While TOML is great for simple configs, it gets clunky when you have deeply nested lists or complex hierarchies. That's where YAML comes in. You'll see YAML everywhere in the Kubernetes and CI/CD world because it handles nesting through indentation rather than explicit section headers. It's a bit more "loose" than TOML, but for deployment manifests or complex data definitions, it's far more readable.

To use it, you'll pull in the serde_yaml crate. The API is almost identical to the TOML crate, which is the beauty of the serde trait system. You just swap the function call.

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Deployment {
    app_name: String,
    replicas: u32,
    env_vars: Vec<EnvVar>,
}

#[derive(Serialize, Deserialize, Debug)]
struct EnvVar {
    key: String,
    value: String,
}

fn main() {
    let deploy = Deployment {
        app_name: "auth-service".to_string(),
        replicas: 3,
        env_vars: vec![
            EnvVar { key: "LOG_LEVEL".into(), value: "debug".into() },
            EnvVar { key: "DB_URL".into(), value: "postgres://...".into() },
        ],
    };

    let yaml_string = serde_yaml::to_string(&deploy).expect("Failed to serialize to YAML");
    println!("YAML Output:\n{}", yaml_string);
}

One thing to keep in mind: YAML's flexibility is a double-edged sword. It has some strange edge cases (like the "Norway Problem" where the country code NO can be parsed as a boolean false). For most application configs, you won't hit this, but if you're dealing with arbitrary user-provided strings, just stay mindful of how YAML interprets types.




📋 Practical Task

Build a Multi-Format Config Porter

You need to create a utility that can take a system configuration struct and export it to both TOML and YAML formats simultaneously. This is common when you need to provide a "default" config file for users (TOML) but also need to generate a deployment manifest for a pipeline (YAML).

Requirements:

  • Define a SystemSettings struct that contains:
    • version (String)
    • features (A Vec<String>)
    • A nested NetworkSettings struct containing ip (String) and port (u16).
  • Implement a function export_settings(settings: &SystemSettings) that prints the serialized TOML and YAML versions of the data to the console, clearly labeled.
  • Ensure you use serde derive macros for Serialize and Deserialize.
  • Handle the potential errors from toml::to_string and serde_yaml::to_string using expect or unwrap for this exercise.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.