🎓 A1Learner
🔍

Rust TOML and YAML with Serde

TOML and YAML are text formats that serde can read and write through format crates. The derive on a struct stays untouched.

The same struct in TOML

Config holds a name and a port. toml::to_string does for TOML what serde_json::to_string does for JSON.

let cfg = Config { name: String::from("Ada"), port: 8080 };
let text = toml::to_string(&cfg).unwrap();
print!("{text}");

TOML text back into a struct

from_str mirrors the JSON spelling. Only the crate name changes.

let cfg: Config = toml::from_str("name = \"Grace\"\nport = 9000").unwrap();
println!("{} {}", cfg.name, cfg.port);

Nested structs become tables

App holds a title and a nested server. TOML groups nested fields under a table header. The header is the field name in brackets.

let app = App {
    title: String::from("shop"),
    server: Server { host: String::from("localhost"), port: 8080 },
};
let text = toml::to_string(&app).unwrap();
print!("{text}");

The same struct in YAML

serde_yaml speaks the same pair of verbs. Its text drops the quotes and the equals signs.

let text = serde_yaml::to_string(&cfg).unwrap();
print!("{text}");

YAML text back into a struct

The reading side mirrors the writing side. The struct fills just like it did from JSON.

let cfg: Config = serde_yaml::from_str("name: Grace\nport: 9000").unwrap();
println!("{} {}", cfg.name, cfg.port);

TOML has no null

TOML cannot say null. A field set to None is simply left out of the text.

#[derive(Serialize)]
struct Cfg {
    name: String,
    nick: Option<String>,
}

let cfg = Cfg { name: String::from("Ada"), nick: None };
print!("{}", toml::to_string(&cfg).unwrap());

Example

// Cargo.toml: serde = { version = "1.0.229", features = ["derive"] }, toml = "0.9.12", serde_yaml = "0.9.34"
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct Server {
    host: String,
    port: u32,
}

#[derive(Serialize, Deserialize, Debug)]
struct App {
    title: String,
    server: Server,
}

fn main() {
    let app = App {
        title: String::from("shop"),
        server: Server { host: String::from("localhost"), port: 8080 },
    };
    let text = toml::to_string(&app).unwrap();
    print!("{text}");
    let back: App = toml::from_str(&text).unwrap();
    println!("{} on port {}", back.title, back.server.port);
    let yaml = serde_yaml::to_string(&back).unwrap();
    print!("{yaml}");
}

Key points

  • Each format lives in its own crate.
  • Add toml or serde_yaml to Cargo.toml and call its functions.
  • The struct never changes.
  • One derive can serve JSON to an API and TOML to a config file.
  • TOML suits files that people edit by hand.
  • Its keys read like a settings page.
  • YAML suits data with deep nesting.
  • Indentation carries the structure, so keep it tidy.
  • Other formats plug in the same way.
  • RON and MessagePack both have serde crates.
  • TOML wants a table at the top level, not a bare list.
  • TOML and YAML carry the same serde data in different text forms.

In this example, one App struct speaks two formats. TOML writes the title first. The nested server then gets its own table. The program reads the TOML back into an App. It prints the title and the port as proof. Last, serde_yaml writes the same struct as YAML. No field changed. Only the format crate changed.

Note

A format can refuse a shape it cannot express.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Your structs already derive Serialize and Deserialize. What must you add to write them as TOML?