Serde: default value for struct field depending on parent

In the following code

use serde::Deserialize;

#[derive(Deserialize)]
struct Foo {
  border: Border,
}

#[derive(Deserialize)]
struct Bar {
  border: Border,
}

#[derive(Deserialize)]
struct Border {
  // Default should be `true` in `Foo`, `false` in `Baz`
  enable: bool,

  // Default should be `a` in `Foo`, `b` in `Baz`
  style: char,
}

I have two structs Foo and Bar, both of which have a border field of type Border.

How can I have different default values for the enable and char fields of the Border struct depending on the parent struct, either Foo or Bar?

This is a tricky problem indeed! I believe that the trick here is to begin by deserializing fields to Options, and then use #[serde(deserialize_with)] to transform the deserialized structures by filling in missing values. Here is a bit of an incomplete solution:

I defined a similar, "parallel" struct to Border called IncompleteBorder, where where every field is an Option. This struct derives Deserialize.

#[derive(Debug, PartialEq, Deserialize)]
struct IncompleteBorder {
    #[serde(default)]
    enable: Option<bool>,

    #[serde(default)]
    style: Option<char>,
}

Next, I used #[serde(deserialize_with)] to let Foo use a transforming function when deserializing the border field.

#[derive(Debug, PartialEq, Deserialize)]
struct Foo {
    #[serde(deserialize_with = "deserialize_foo_border")]
    border: Border,
}

fn deserialize_foo_border<'de, D>(deserializer: D) -> Result<Border, D::Error>
where
	D: Deserializer<'de>,
{
    Deserialize::deserialize(deserializer)
        .map(|IncompleteBorder { enable, style }| Border {
            enable: enable.unwrap_or(true),
            style: style.unwrap_or('a'),
        })
}

and similar for Bar.

Now, there's a catch. This will correctly handle stuff like:

{ "border": {"style": "Q"} }

but... if you're doing anything like I suspect you're doing with these types, then I imagine you probably also want it to support the case where the "border" field is not present. So I also added a #[serde(default)] on the border: Border field of Foo, telling it to deserialize as if there were a "border": {}.

#[derive(Debug, PartialEq, Deserialize)]
struct Foo {
    #[serde(deserialize_with = "deserialize_foo_border")]
    #[serde(default = "default_foo_border")]  // <---- ADDED
    border: Border,
}

fn empty_json_object() -> impl for<'de> Deserializer<'de> {
    Value::Object(Map::new()).into_deserializer()
}

fn default_foo_border() -> Border {
    deserialize_foo_border(empty_json_object()).unwrap()
}

Playground with tests: Rust Playground


Now, there are two issues with my design above:

  1. It's boilerplatey. You'll absolutely want to write a macro at some point to automate this.
  2. There's something clearly wrong with the current design in that it doesn't compose naturally.

What do I mean by the second point? Basically it's not yet clear to me at this point how one would do this for a larger tree of structs with more levels of nesting. The way I have currently written this, it is strange how a missing "border" field is handled differently from a missing "enable" field. Why does Foo derive Deserialize while Border does not? It seems like there ought to be IncompleteFoo and IncompleteBar types, and etc. Unfortunately I do not have the leisure right now to dig further into this and discover the "true" pattern that the code above is only approximating.

Anyways, hopefully this has given you some ideas.

1 Like

Awesome! Just gave you a shoutout on the latest commit :muscle:.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.