Serde and Nested Maps
In one of my Rust projects I am parsing very nested JSON structures. Let’s use the following example as our data source (though in reality it is 20+ deep):
{
"foo": {
"bar": [
{
"baz": {
"x": "y"
}
}
]
}
}
Serde is the de-facto framework for serializing and deserializing data efficiently in Rust. It supports parsing JSON into either strongly typed data structures or untyped JSON values.
Rust does not have a way to define anonymous structs, so for the strongly typed approach we would need to define 20+ separate structs to get the data we want. No, thank you.
Fortunately, accessing anonymous structs is quite ergonomic.
let data: Value = serde_json::from_str(input)?;
let x: &Value = &data["foo"]["bar"][0]["baz"]["x"];
Additionally, if there is a missing index the result will be Null
(I
originally thought it would panic)!
x = String("y") // happy case
x = Null // sad case
For my project I would like to know which index was missing,
however, so I can quickly debug and provide a fix. In
addition to indexing with square brackets, Serde also has a
get
method that returns an Option<&Value>
. So now we can do:
let x: Result<&Value, &'static str> = data.get("foo").ok_or("missing foo")
.and_then(|v| v.get("bar").ok_or("missing bar"))
.and_then(|v| v.get(0).ok_or("missing 0"))
.and_then(|v| v.get("baz").ok_or("missing baz"))
.and_then(|v| v.get("x").ok_or("missing x"));
We have more information, but at the expense of verbosity. The pattern is quite regular, so I’m sure we can make it a macro though! I won’t pretend to be an expert on Rust macros, but I cobbled together this one:
{% raw %}
macro_rules! get {
($value:ident, $first:expr) => {{
$value.get($first).ok_or(concat!("missing ", $first))
}};
($value:ident, $first:expr, $($( $key:expr )+$(,)?)*) => {{
get!($value, $first)
$($( .and_then(|v| get!(v, $key)) )*)*
}};
}
{% endraw %}
and it can be used like so:
let x: Result<&Value, &'static str> = get!(data, "foo", "bar", 0, "baz", "x");
println!("x = {:?}", x);
x = Ok(String("y")) // happy case
x = Err("missing bar") // sad case
Now, we have an ergonomic way to access nested data with information on exactly where things go awry.