How to make an `Option` to be None by inner field in serde?

Viewed 438
use serde_derive::Deserialize; // 1.0.118
use toml; // 0.5.8

#[derive(Debug, Deserialize)]
struct Remote {
    enabled: bool,
    address: String,
}

#[derive(Debug, Deserialize)]
struct Config {
    remote: Option<Remote>,
}


fn main() {
    let config: Config = toml::from_str(r#"
    [remote]
    enabled = true
    address = "example.com"
    "#).unwrap();
    println!("{:?}", config);

    let config: Config = toml::from_str(r#"
    [remote]
    enabled = false
    address = "example.com"
    "#).unwrap();
    println!("{:?}", config);
    // How to make `config.remote` `None` ?
}

Playground link

I want the rest fields of Remote to be optional and Config::remote to be None when enabled is set to false.

I tried tag attribute but didn't work as expected.

2 Answers

Believe it or not, you actually can do this using the #[serde(deserialize_with = "...")] attribute. It lets you override the deserializer applied to a certain field. The goal here is to use an overriding deserializer that calls the regular deserializer, then checks the enabled field manually. This is (thinly) documented on the serde website.

First, the code:

// Important: Deserialize is needed to access T::deserialize() methods.
use serde::{Deserialize, Deserializer};

fn none_if_disabled<'de, D>(deserializer: D) -> Result<Option<Remote>, D::Error>
where
    D: Deserializer<'de>,
{
    let remote = Option::<Remote>::deserialize(deserializer)?;

    Ok(remote.and_then(|r| if r.enabled { Some(r) } else { None }))
}

#[derive(Debug, Deserialize)]
struct Remote {
    enabled: bool,
    address: String,
}

#[derive(Debug, Deserialize)]
struct Config {
    #[serde(deserialize_with = "none_if_disabled")]
    remote: Option<Remote>,
}

Rust Playground

Lets break it down. The function signature is a little gnarly if you're not familiar with serde's internals.

fn none_if_disabled<'de, D>(deserializer: D) -> Result<Option<Remote>, D::Error>
where
    D: Deserializer<'de>,

D is something that implements Deserializer. This is generally provided by the parser crate, in this case toml, though other parsers have their own. We can ask D to try and give us various primitives, or we can pass it into something that implements Deserialize, which will consume these primitives for us. We need to return either the successful parsed value (Option<Remote>) or bubble up any errors that we get.

Inside the function, the first step is to use the regular Deserialize impl that we got from #[derive(Deserialize)] on Remote. This is the "normal" parsing step, and is roughly what would be called if we didn't have a deserialize_with attribute.

let remote = Option::<Remote>::deserialize(deserializer)?;

Then, we need the filtering logic. This can be as complex as you need it to be, as long as it outputs a Result<Option<Remote>, _>.

Ok(remote.and_then(|r| if r.enabled { Some(r) } else { None }))

Finally, we need to tell serde about our fancy deserializer.

#[serde(deserialize_with = "none_if_disabled")]

The output with your main function is:

Config { remote: Some(Remote { enabled: true, address: "example.com" }) }
Config { remote: None }

This works, but only with a bunch of additional complexity. @Skarlett is completely correct that serde isn't really built for this kind of configuration management, and you might be better off with a post-processing pass on the stuff serde parses for you directly. My preferred approach would be to keep all of your Remotes in place and explicitly change behavior based on the enabled flag -- this allows you to do things with the disabled configurations, such as explicitly logging that a connection was not attempted.

That said, these uses of serde do have a place, even if a configuration file probably isn't it.

Related