Skip to content
Select theme

How to read a YAML file

Short answer: Read the file with fs::read_to_string and hand the string to serde_yaml::from_str, typed as a struct that derives Deserialize — the JSON page with serde_yaml in place of serde_json. Both traps live in the YAML itself: no is a string, and an unquoted 1.10 is the float 1.1.

This continues the json-demo project from the CSV pages; add one line to its Cargo.toml:

Cargo.toml
[dependencies]
serde = { version = "1.0.229", features = ["derive"] }
serde_yaml = "0.9.34"

serde_yaml takes no feature flags; cargo add serde_yaml --features derive answers “no features available for crate serde_yaml”.

src/main.rs
use serde::Deserialize;
use std::fs;
#[derive(Debug, Deserialize)]
struct Config {
name: String,
port: u16,
debug: bool,
}
fn main() {
fs::write("config.yaml", "name: yamlread\nport: 8080\ndebug: true\n").unwrap();
let text = fs::read_to_string("config.yaml").unwrap();
let config: Config = serde_yaml::from_str(&text).unwrap();
println!("{config:?}");
}
Config { name: "yamlread", port: 8080, debug: true }

The fs::write line only gives the block a file to read — in real code the file is already there. from_str borrows the string, so read once and parse; serde_yaml::from_reader does the same job on any io::Read.

The Norway problem is that YAML 1.1 reads an unquoted no as false. serde_yaml 0.9 resolves plain scalars by the YAML 1.2 core schema, where the booleans are true and false, so no arrives as a string:

src/main.rs
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Flags {
verbose: bool,
}
#[derive(Debug, Deserialize)]
struct Names {
verbose: String,
}
fn main() {
let yaml = "verbose: no\n";
match serde_yaml::from_str::<Flags>(yaml) {
Ok(flags) => println!("bool field: {flags:?}"),
Err(e) => println!("bool field: error: {e}"),
}
match serde_yaml::from_str::<Names>(yaml) {
Ok(names) => println!("String field: {names:?}"),
Err(e) => println!("String field: error: {e}"),
}
}
bool field: error: verbose: invalid type: string "no", expected a boolean at line 1 column 10
String field: Names { verbose: "no" }

That is a disagreement between tools, not a bug in either. Python’s PyYAML 6.0.3 (on Python 3.13.12) still reads the line the YAML 1.1 way:

Terminal window
$ python3 -c "import yaml; print(yaml.safe_load('verbose: no')); print(yaml.safe_load('version: 1.10'))"
{'verbose': False}
{'version': 1.1}

A file shared between the two tools means false on one side and the string "no" on the other. Write true or false for a bool field, and quote everything else.

Version strings are the usual victim. version: 1.10 is a float scalar, so the trailing zero does not survive the trip:

src/main.rs
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct AsFloat {
version: f64,
}
#[derive(Debug, Deserialize)]
struct AsText {
version: String,
}
fn main() {
let yaml = "version: 1.10\n";
let f: AsFloat = serde_yaml::from_str(yaml).unwrap();
let t: AsText = serde_yaml::from_str(yaml).unwrap();
println!("f64 field: {f:?}");
println!("String field: {t:?}");
let value: serde_yaml::Value =
serde_yaml::from_str("port: 0x10\nsize: 1e3\nlimit: .inf\n").unwrap();
println!("{value:?}");
}
f64 field: AsFloat { version: 1.1 }
String field: AsText { version: "1.10" }
Mapping {"port": Number(16), "size": Number(1000.0), "limit": Number(.inf)}

Into a String field the raw text survives, so the bug hides until the field turns numeric or the document goes through serde_yaml::Value. PyYAML prints 1.1 as well — this trap belongs to the format, not to Rust. Hex (0x10), exponents (1e3) and .inf are numbers too. Quote anything meant as text.

src/main.rs
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Config {
name: String,
port: u16,
}
fn main() {
for (label, yaml) in [
("bad indent", "name: a\n port: 80\n"),
("tab indent", "name: a\n\tport: 80\n"),
("missing field", "name: a\n"),
("duplicate key", "name: a\nname: b\n"),
] {
match serde_yaml::from_str::<Config>(yaml) {
Ok(config) => println!("{label}: {config:?}"),
Err(e) => println!("{label}: {e}"),
}
}
}
bad indent: mapping values are not allowed in this context at line 2 column 7
tab indent: found a tab character that violates indentation at line 2 column 1, while scanning a plain scalar at line 1 column 7
missing field: missing field `port`
duplicate key: duplicate field `name`

Parser errors name a position — at line 2 column 7 — and the ones raised mid-scalar add what the scanner was doing. The last two come from serde’s derive, not the YAML parser, and carry no position at all. An empty file reports missing field too, and a file holding several ----separated documents is refused outright: “deserializing from YAML containing more than one document is not supported”.

serde_yaml 0.9.34 is the last release. The version on crates.io is 0.9.34+deprecated, the README says “(This project is no longer maintained.)”, and the release note says why: “I am not planning to publish further versions of serde_yaml … I have archived the GitHub repo and marked the crate deprecated in the version number.” Nothing has been yanked and existing code keeps building, but no official replacement is designated — the note points at crates.io’s YAML search, where serde_norway (0.9.42) and serde-saphyr (2.0.0) are two of the options.

The next page in this chapter parses query strings.