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:
- It's boilerplatey. You'll absolutely want to write a macro at some point to automate this.
- 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.