Set default values while deserializing YAML in Rust

YAML default serde

We saw how to read a deserialize a simple YAML into a struct. What happens if some of the fields we are expecting in the YAML file are missing?

For the following example we created a struct like this

#[derive(Deserialize)]
struct Person {
    name: String,
    email: String,
    year: u32,
    married: bool,
}

If se supply the following YAML file, each field is filled.

examples/yaml-default-values/all.yaml

name: Foo Bar
email: foo@bar.com
year: 1990
married: true

However if we provide the following file:

examples/yaml-default-values/nameless.yaml

email: foo@bar.com
year: 1990
married: true

We will get an error message in the err variable:

missing field `name`

We get a similar error if more than one field is missing:

examples/yaml-default-values/data.yaml

name: Foo Bar

Set the default values

One of the solutions is to set default values for some or all of the fields. We can do that by using the default attribute and passing in the name of a function that is going to return the default value.

#[derive(Deserialize)]
struct Person {
    name: String,

    #[serde(default = "get_default_email")]
    email: String,

    #[serde(default = "get_default_year")]
    year: u32,

    #[serde(default = "get_default_married")]
    married: bool,
}

fn get_default_email() -> String {
    String::from("default@address")
}

fn get_default_year() -> u32 {
    2000
}

fn get_default_married() -> bool {
    false
}

The full example

examples/yaml-default-values/src/main.rs

use serde::Deserialize;
use std::fs;

#[derive(Deserialize)]
struct Person {
    name: String,

    #[serde(default = "get_default_email")]
    email: String,

    #[serde(default = "get_default_year")]
    year: u32,

    #[serde(default = "get_default_married")]
    married: bool,
}

fn get_default_email() -> String {
    String::from("default@address")
}

fn get_default_year() -> u32 {
    2000
}

fn get_default_married() -> bool {
    false
}

fn main() {
    let filename = get_filename();
    let text = fs::read_to_string(filename).unwrap();

    let data: Person = serde_yaml::from_str(&text).unwrap_or_else(|err| {
        eprintln!("Could not parse YAML file: {err}");
        std::process::exit(1);
    });

    println!("name: {}", data.name);
    println!("email: {}", data.email);
    println!("year: {}", data.year);
    println!("married: {}", data.married);
}

fn get_filename() -> String {
    let args: Vec<String> = std::env::args().collect();
    if args.len() != 2 {
        eprintln!("Usage: {} FILENAME", args[0]);
        std::process::exit(1);
    }
    args[1].to_string()
}

In this example we have a function called get_filename that gets the name of the file from the command line.

A problem - what if we have a typo?

What if this is the YAML file

examples/yaml-default-values/typo.yaml

name: Foo Bar
email: foo@bar.com
year: 1990
maried: true

Have you noticed the typo I made in one of the fields? I typed in "maried" instead of "married", but I could have mixed up the field called "color" and typed in "colour", if there indeed was such a field.

The current code will happily disregard the field with the typo and use the default value for the "married" field.

That's not ideal.

Dependencies

See the Cargo.toml` we had:

examples/yaml-default-values/Cargo.toml

[package]
name = "yaml-default-values"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"

Related Pages

YAML and Rust
Deserializing YAML - deny unknown fields

Author

Gabor Szabo (szabgab)

Gabor Szabo, the author of the Rust Maven web site maintains several Open source projects in Rust and while he still feels he has tons of new things to learn about Rust he already offers training courses in Rust and still teaches Python, Perl, git, GitHub, GitLab, CI, and testing.

Gabor Szabo