使用Serde将两种类型转换为单一类型

16

我正在编写一个程序,该程序钩入到一个发送JSON响应的Web服务中。

当某个属性不存在时,它提供一个空对象,其中所有字段都为空字符串,而不是排除该值。当该属性存在时,某些属性为u64。我如何让Serde处理这种情况?

Rust结构体

#[derive(Clone, Debug, Deserialize)]
struct WebResponse {
    foo: Vec<Foo>,
}

#[derive(Clone, Debug, Deserialize)]
struct Foo {
    points: Points,
}

#[derive(Clone, Debug, Deserialize)]
struct Points {
    x: u64,
    y: u64,
    name: String,
}

示例 JSON

{
    "foo":[
        {
            "points":{
                "x":"",
                "y":"",
                "name":""
            }
        },
        {
            "points":{
                "x":78,
                "y":92,
                "name":"bar"
            }
        }
    ]
}

当您收到一个空字符串而不是u64时,应该发生什么?您想将字段设置为0吗?还是None?还是其他什么? - Francis Gagné
@FrancisGagné 我认为它应该是 0 - XAMPPRocky
3个回答

18

Serde支持一系列有趣的属性,可用于自定义类型的序列化或反序列化,同时仍然使用大部分派生实现。

在您的情况下,您需要能够解码可以指定为多种类型之一的字段,并且您不需要来自其他字段的信息来决定如何解码有问题的字段。 #[serde(deserialize_with="$path")]注释非常适合解决您的问题。

我们需要定义一个函数,将空字符串或整数值解码为u64。 我们可以为两个字段使用相同的函数,因为我们需要相同的行为。 该函数将使用自定义访问者以处理字符串和整数。 它有点长,但它让您欣赏Serde为您完成的所有工作!

extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;

use serde::Deserializer;
use serde::de::{self, Unexpected};
use std::fmt;

#[derive(Clone, Debug, Deserialize)]
struct WebResponse {
    foo: Vec<Foo>,
}

#[derive(Clone, Debug, Deserialize)]
struct Foo {
    points: Points,
}

#[derive(Clone, Debug, Deserialize)]
struct Points {
    #[serde(deserialize_with = "deserialize_u64_or_empty_string")]
    x: u64,
    #[serde(deserialize_with = "deserialize_u64_or_empty_string")]
    y: u64,
    name: String,
}

struct DeserializeU64OrEmptyStringVisitor;

impl<'de> de::Visitor<'de> for DeserializeU64OrEmptyStringVisitor {
    type Value = u64;

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        formatter.write_str("an integer or a string")
    }

    fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
    where
        E: de::Error,
    {
        Ok(v)
    }

    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    where
        E: de::Error,
    {
        if v == "" {
            Ok(0)
        } else {
            Err(E::invalid_value(Unexpected::Str(v), &self))
        }
    }
}

fn deserialize_u64_or_empty_string<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
    D: Deserializer<'de>,
{
    deserializer.deserialize_any(DeserializeU64OrEmptyStringVisitor)
}

fn main() {
    let value = serde_json::from_str::<WebResponse>(
        r#"{
        "foo": [
            {
                "points": {
                    "x": "",
                    "y": "",
                    "name": ""
                }
            },
            {
                "points": {
                    "x": 78,
                    "y": 92,
                    "name": "bar"
                }
            }
        ]
    }"#,
    );
    println!("{:?}", value);
}

Cargo.toml:

[dependencies]
serde = "1.0.15"
serde_json = "1.0.4"
serde_derive = "1.0.15"

11
str_or_u64 中,我们使用一个 无标签枚举 来表示字符串或数字。然后,我们可以将该字段反序列化为该枚举并转换为数字。
Points 中,我们使用deserialize_with 注释两个字段,以告诉它使用我们的特殊转换方法:
use serde::{Deserialize, Deserializer}; // 1.0.124
use serde_json; // 1.0.64

#[derive(Debug, Deserialize)]
struct WebResponse {
    foo: Vec<Foo>,
}

#[derive(Debug, Deserialize)]
struct Foo {
    points: Points,
}

#[derive(Debug, Deserialize)]
struct Points {
    #[serde(deserialize_with = "str_or_u64")]
    x: u64,
    #[serde(deserialize_with = "str_or_u64")]
    y: u64,
    name: String,
}

fn str_or_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
    D: Deserializer<'de>,
{
    #[derive(Deserialize)]
    #[serde(untagged)]
    enum StrOrU64<'a> {
        Str(&'a str),
        U64(u64),
    }

    Ok(match StrOrU64::deserialize(deserializer)? {
        StrOrU64::Str(v) => v.parse().unwrap_or(0), // Ignoring parsing errors
        StrOrU64::U64(v) => v,
    })
}

fn main() {
    let input = r#"{
        "foo":[
            {
                "points":{
                    "x":"",
                    "y":"",
                    "name":""
                }
            },
            {
                "points":{
                    "x":78,
                    "y":92,
                    "name":"bar"
                }
            }
        ]
    }"#;

    dbg!(serde_json::from_str::<WebResponse>(input));
}

另请参阅:


1

为了未来的观众,我添加了一条注释:如果有帮助的话,我已经实现了接受答案的解决方案,并将其发布为一个crate serde-this-or-that

我添加了一个性能部分,以解释建议使用自定义Visitor的方法应该比使用未标记枚举的版本表现更好,尽管后者也可以工作。

这是上面接受的解决方案的缩短实现(应该具有相同的结果):

use serde::Deserialize;
use serde_json::from_str;
use serde_this_or_that::as_u64;

#[derive(Clone, Debug, Deserialize)]
struct WebResponse {
    foo: Vec<Foo>,
}

#[derive(Clone, Debug, Deserialize)]
struct Foo {
    points: Points,
}

#[derive(Clone, Debug, Deserialize)]
struct Points {
    #[serde(deserialize_with = "as_u64")]
    x: u64,
    #[serde(deserialize_with = "as_u64")]
    y: u64,
    name: String,
}

fn main() {
    let value = from_str::<WebResponse>(
        r#"{
        "foo": [
            {
                "points": {
                    "x": "",
                    "y": "",
                    "name": ""
                }
            },
            {
                "points": {
                    "x": 78,
                    "y": 92,
                    "name": "bar"
                }
            }
        ]
    }"#,
    );
    println!("{:?}", value);
}

Cargo.toml 的样子如下:

[dependencies]
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
serde-this-or-that = "0.4"

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