I want to deserialize the chemical elements JSON file from Bowserinator on github using Serde. For this I created a structure with all the needed fields and derived the needed macros:
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Element {
name: String,
appearance: String,
atomic_mass: f64,
boil: f64,
category: String,
#[serde(default)]
color: String,
density: f64,
discovered_by: String,
melt: f64,
#[serde(default)]
molar_heat: f64,
named_by: String,
number: String,
period: u32,
phase: String,
source: String,
spectral_img: String,
summary: String,
symbol: String,
xpos: u32,
ypos: u32,
}
This works fine until it gets to fields which contain a "null" value. Eg for the field "color": null,
in Helium.
The error message I get is { code: Message("invalid type: unit value, expected a string"), line: 8, column: 17 }
for this field.
I experimented with the #[serde(default)]
Macro. But this only works when the field is missing in the JSON file, not when there is a null
value.
I like to do the deserialization with the standard macros avoiding to program a Visitor Trait. Is there a trick I miss?
A deserialization error occurs because the struct definition is incompatible with the incoming objects: the color
field can also be null
, as well as a string, yet giving this field the type String
forces your program to always expect a string. This is the default behaviour, which makes sense. Be reminded that String
(or other containers such as Box
) are not "nullable" in Rust. As for a null
value not triggering the default value instead, that is just how Serde works: if the object field wasn't there, it would work because you have added the default field attribute. On the other hand, a field "color" with the value null
is not equivalent to no field at all.
One way to solve this is to adjust our application's specification to accept null | string
null | string
, as specified by @user25064's answer:
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Element {
color: Option<String>,
}
Playground with minimal example
Another way is to write our own deserialization routine for the field, which will accept null
and turn it to something else of type String
. This can be done with the attribute #[serde(deserialize_with=...)]
.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Element {
#[serde(deserialize_with="parse_color")]
color: String,
}
fn parse_color<'de, D>(d: D) -> Result<String, D::Error> where D: Deserializer<'de> {
Deserialize::deserialize(d)
.map(|x: Option<_>| {
x.unwrap_or("black".to_string())
})
}
See also:
Any field that can be null should be an Option
type so that you can handle the null case. Something like this?
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Element {
...
color: Option<String>,
...
}
Based on code from here , when one needs default values to be deserialized if null
is present.
// Omitting other derives, for brevity
#[derive(Deserialize)]
struct Foo {
#[serde(deserialize_with = "deserialize_null_default")]
value: String,
}
fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
T: Default + Deserialize<'de>,
D: Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
playground link with full example. This also works for Vec
and HashMap
.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.