Don't serialize default values

By default, serde includes all fields when serializing a struct, even when their value is the default. This can lead to noisy output containing empty values.

(Feel free to skip this introduction.)

For example, we are building a command-line application. We have a struct to hold configuration.

use std::path::PathBuf;

#[derive(Debug, Default)]
struct Configuration {
    a: String,       // Please find some better names
    b: Vec<PathBuf>, // for your configuration fields.
    c: String,
}

We prompt the user for information and want to write it to a configuration file in YAML format.

use std::io;

fn main() -> io::Result<()> {
    // Prompt the user for the value of `a`.
    print!("a: ");
    io::stdout().flush()?;
    let mut a = String::new();
    io::stdin().read_line(&mut a)?;
    // Not really the best way since we allocate another `String`.
    let a = a.trim().to_owned();

    let config = Configuration {
        a, // short for `a: a`
        ..Configuration::default() // take all other fields from there
    };

    // Serialize the configuration.
    // ...

    Ok(())
}

For that, we'll use the serde and serde_yaml crates, and serde's optional derive feature. In our crate's Cargo.toml, we add:

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

We derive the Serialize trait for our struct, as well as Deserialize since we'll want to read and parse the configuration file later. The fields b and c are not mandatory, so we apply the serde field attribute default.

use std::path::PathBuf;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug, Default)]
struct Configuration {
    a: String,
    #[serde(default)] // this only affects deserialization
    b: Vec<PathBuf>,
    #[serde(default)]
    c: String,
}

Now, in main, we serialize our config and write it to a file.

use std::io::{self, Write};
use std::fs;

fn main() -> io::Result<()> {
    // Prompt the user for the value of `a`.
    print!("a: ");
    io::stdout().flush()?;
    let mut a = String::new();
    io::stdin().read_line(&mut a)?;
    // Not really the best way since we allocate another `String`.
    let a = a.trim().to_owned();

    let config = Configuration {
        a, // short for `a: a`
        ..Configuration::default() // take all other fields from there
    };
    
    let config = serde_yaml::to_string(&config).unwrap();
    fs::File::create("config.yaml")?
        .write_fmt(format_args!("{}", config))?;

    Ok(())
}

After running the program, config.yaml contains:

---
a: i typed this in
b: []
c: ""

We want to get rid of b: [] and c: "". (Granted, this may not look so bad in this example. It starts to get ugly with larger configuration files.) Generally, we want to skip serialization if the value of a field is the default.

To that end, we use the skip_serializing_if field attribute. It can be assigned a function that accepts a reference to the field's value and returns a bool.

use std::path::PathBuf;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug, Default)]
struct Configuration {
    a: String,
    #[serde(default, skip_serializing_if = "is_default")]
    b: Vec<PathBuf>,
    #[serde(default, skip_serializing_if = "is_default")]
    c: String,
}

Our is_default function will accept any t: &T where T satisfies the traits Default (so we can fetch the default value), and PartialEq (so we can compare it to ours with ==).

fn is_default<T: Default + PartialEq>(t: &T) -> bool {
    t == &T::default()
}

After running the program again, config.yaml contains:

---
a: rust is fun