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