如何使用Serde反序列化一个包含null值的JSON文件?

32

我想使用Serde从github上Bowserinator的库反序列化元素周期表的JSON文件。为此,我创建了一个拥有所有必要字段的结构并派生了所需的宏:

#[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,
}

这个方法在遇到包含“null”值的字段时会出现问题。例如,在Helium中,对于字段"color": null

我收到的错误消息是{ code: Message("invalid type: unit value, expected a string"), line: 8, column: 17 },针对此字段。

我尝试使用#[serde(default)]宏进行实验。但是,当JSON文件中存在null值时,它仅适用于字段缺失的情况。

我希望能够使用标准宏进行反序列化,而不是编写Visitor Trait。是否有我忽略的技巧?


1
强烈建议您阅读《Rust编程语言》,该书介绍了OptionResult的概念,这在Rust中非常普遍。(链接) - Shepmaster
1
我已经做了这个,但是提示如何处理这种情况会很有帮助,因为似乎我需要换一种思路。就像我上面说的,我的假设是我需要实现Visitor Trait,并且我想避免那样做。正如我下面所说:我也想避免第二次解析所有读取的结构,希望Serde有某种魔法可以帮助。 - Hartmut
如果您提供了一个 [MCVE],那么您的问题会更清晰明了。目前为止,您已经提供了代码和输入,但是没有说明您想要什么输出结果。正如您所看到的,您提出的模糊问题导致了两个截然不同的答案。 - Shepmaster
好的,谢谢,下次我会这样做。 - Hartmut
3个回答

48
发生反序列化错误是因为结构体定义与传入的对象不兼容:color字段也可以是null,也可以是string类型,但如果将此字段类型设置为String,则程序将始终期望一个字符串。这是默认行为,很有道理。请记住,在Rust中,String(或其他容器,如Box)不可为空。至于为什么null值没有触发默认值,这就是Serde的工作方式:如果对象字段不存在,则它会起作用,因为您已经添加了默认字段属性。另一方面,带有null值的“color”字段与根本没有该字段不等价。 解决此问题的一种方法是调整应用程序规格以接受null | string,如@user25064的答案所指定的那样:
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Element {
    color: Option<String>,
}

使用最小的示例来试玩

另一种方法是编写我们自己的反序列化程序来处理该字段,它将接受 null 并将其转换为其他类型的 String。这可以通过属性 #[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())
        })
}

游乐场

另请参阅:


谢谢,特别是解释得非常清楚。我想我会选择第二种方式,这样我就可以避免使用一个翻译类(从一个带有Option<T>的结构到我喜欢的结构)。 - Hartmut
我真的希望Serde能以更好的方式处理这个问题。在Rust中,null不是一个有效的值,但在JSON中却是一个有效的值,因此Serde应该实现基本的JSON标准。目前的解决方案都很冗长且不够优秀。要么为每个字段使用Option<...>(然后为每个字段使用unwrap_or_else,嗯...),或者为每个字段添加#[serde(deserialize_with="...")],这似乎实际上可能更好些。 - Christophe Vidal

8

任何可能为空的字段都应该是一个Option类型,这样你才能处理空的情况。就像这样:

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Element {
    ...
    color: Option<String>,
    ...
}

我希望有一些技巧可以自动化这个转换过程。我想避免在从Serde解析器返回Element结构后再次解析它并修复所有空值。 - Hartmut

6

基于这里的代码,当出现null时需要反序列化默认值。

// 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())
}

示例链接,可供参考。此方法也适用于 VecHashMap


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接