如何使用Serde将具有结构体键的HashMap序列化为JSON?

41

我想将一个以结构体为键的 HashMap 进行序列化:

use serde::{Deserialize, Serialize}; // 1.0.68
use std::collections::HashMap;

fn main() {
    #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
    struct Foo {
        x: u64,
    }

    #[derive(Serialize, Deserialize, Debug)]
    struct Bar {
        x: HashMap<Foo, f64>,
    }

    let mut p = Bar { x: HashMap::new() };
    p.x.insert(Foo { x: 0 }, 0.0);
    let serialized = serde_json::to_string(&p).unwrap();
}

这段代码可以编译,但是运行时出现了错误:

Error("key must be a string", line: 0, column: 0)'

我修改了代码:

#[derive(Serialize, Deserialize, Debug)]
struct Bar {
    x: HashMap<u64, f64>,
}

let mut p = Bar { x: HashMap::new() };
p.x.insert(0, 0.0);
let serialized = serde_json::to_string(&p).unwrap();

HashMap中的键现在是u64而不是字符串。为什么第一个代码会报错?


5
你知道JSON明确要求使用字符串作为键吗?(来源:维基百科)Serde可能知道如何将u64转换为字符串,但不知道如何处理你的结构体。 - MB-F
2
@kazemakase 我已经意识到了。因此,我为Foo结构派生了Serialize特性。 - YjyJeff
@Boiethios 谢谢!我修复了我的示例代码。 - YjyJeff
@dtolnay 实际上,Foo在我的代码中是一个枚举类型。该枚举类型包含了u64和字符串。我希望HashMap能够将u64或字符串作为键,并将f64作为值进行存储。例如: {"key": 0.5, "9": 0.6} - YjyJeff
4个回答

20
您可以使用serde_with中的serde_as,将HashMap编码为键值对序列:
use serde_with::serde_as; // 1.5.1

#[serde_as]
#[derive(Serialize, Deserialize, Debug)]
struct Bar {
    #[serde_as(as = "Vec<(_, _)>")]
    x: HashMap<Foo, f64>,
}

这将被序列化为(并从中反序列化):

{
  "x":[
    [{"x": 0}, 0.0],
    [{"x": 1}, 0.0],
    [{"x": 2}, 0.0]
  ]
}

HashMap 转换为 Vec 可能会产生一些开销,但这样做非常方便。


16
根据JSON规范,JSON键必须是字符串。serde_json在此处使用fmt::Display,对于一些非字符串键,以便序列化更广泛的HashMaps。这就是为什么HashMap<u64, f64>可以正常工作,就像HashMap<String, f64>一样。但是,并非所有类型都被覆盖(Foo的情况在这里)。
这就是为什么我们需要提供自己的Serialize实现:
impl Display for Foo {
    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
        write!(f, "{}", self.x)
    }
}

impl Serialize for Bar {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut map = serializer.serialize_map(Some(self.x.len()))?;
        for (k, v) in &self.x {
            map.serialize_entry(&k.to_string(), &v)?;
        }
        map.end()
    }
}

(游乐场)


7
这并没有真正回答问题。他为自己的类型导出了serde实现,所以应该已经足够了。 - Steven Roose
限制在于 JSON 序列化器方面。它要求键必须是 String(或更具体地说是 Display)。 另一方面,它会包含什么行为?看起来像字符串化的结构体的键在这种情况下不太好看,可能会产生误导。 - dotPoozer
这个绝对有用,我之前在序列化BTreeMap时遇到了一些问题,做了一些小改动后,现在完美运行! - andresvsm

5

我找到了一种防弹解决方案

  • 不需要额外的依赖项
  • 兼容HashMapBTreeMap和其他可迭代类型
  • flexbuffers一起使用

以下代码将一个字段(映射)转换为中间Vec表示:

pub mod vectorize {
    use serde::{Deserialize, Deserializer, Serialize, Serializer};
    use std::iter::FromIterator;

    pub fn serialize<'a, T, K, V, S>(target: T, ser: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
        T: IntoIterator<Item = (&'a K, &'a V)>,
        K: Serialize + 'a,
        V: Serialize + 'a,
    {
        let container: Vec<_> = target.into_iter().collect();
        serde::Serialize::serialize(&container, ser)
    }

    pub fn deserialize<'de, T, K, V, D>(des: D) -> Result<T, D::Error>
    where
        D: Deserializer<'de>,
        T: FromIterator<(K, V)>,
        K: Deserialize<'de>,
        V: Deserialize<'de>,
    {
        let container: Vec<_> = serde::Deserialize::deserialize(des)?;
        Ok(T::from_iter(container.into_iter()))
    }
}

只需将模块的名称作为属性添加即可使用:

#[derive(Debug, Serialize, Deserialize)]
struct MyComplexType {
    #[serde(with = "vectorize")]
    map: HashMap<MyKey, String>,
}

如果您想在本地检查剩余部分,请参考以下步骤:
use anyhow::Error;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct MyKey {
    one: String,
    two: u16,
    more: Vec<u8>,
}

#[derive(Debug, Serialize, Deserialize)]
struct MyComplexType {
    #[serde(with = "vectorize")]
    map: HashMap<MyKey, String>,
}

fn main() -> Result<(), Error> {
    let key = MyKey {
        one: "1".into(),
        two: 2,
        more: vec![1, 2, 3],
    };
    let mut map = HashMap::new();
    map.insert(key.clone(), "value".into());
    let instance = MyComplexType { map };
    let serialized = serde_json::to_string(&instance)?;
    println!("JSON: {}", serialized);
    let deserialized: MyComplexType = serde_json::from_str(&serialized)?;
    let expected_value = "value".to_string();
    assert_eq!(deserialized.map.get(&key), Some(&expected_value));
    Ok(())
}

在 Rust 游乐场上也可以尝试:https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=bf1773b6e501a0ea255ccdf8ce37e74d


最后,我将解决方案发布为crate,因为需要多次使用:https://crates.io/crates/vectorize - DenisKolodin

1

虽然所有提供的答案都可以实现将您的HashMap序列化为json,但它们是临时的或难以维护的。

允许特定数据结构作为映射中的键与serde一起序列化的一种正确方法是与HashMap中的整数键处理方式相同(有效):将值序列化为String。这有几个优点,即

  1. 省略了中间数据结构,
  2. 无需克隆整个HashMap
  3. 通过应用面向对象编程(OOP)概念更易于维护,以及
  4. 可在更复杂的结构(如MultiMap)中使用序列化。

编辑:创建serde_jdon_any_key是实现此目的最高效的方法。感谢@HighCommander4指出该创建。

或者,可以使用手动实现:

这可以通过手动实现您的数据类型的SerializeDeserialize来完成。

我在地图上使用组合ID。

#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct Proj {
    pub value: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct Doc {
    pub proj: Proj,
    pub value: u32,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct Sec {
    pub doc: Doc,
    pub value: u32,
}

所以现在为它们手动实现serde序列化有点麻烦,因此我们将实现委托给FromStrFrom<Self> for StringInto<String>概述)trait。

impl From<Doc> for String {
    fn from(val: Doc) -> Self {
        format!("{}{:08X}", val.proj, val.value)
    }
}
impl FromStr for Doc {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match parse_doc(s) {
            Ok((_, p)) => Ok(p),
            Err(e) => Err(e.to_string()),
        }
    }
}

为了解析Doc,我们使用nom。下面的解析功能在他们的示例中有解释。
fn is_hex_digit(c: char) -> bool {
    c.is_digit(16)
}

fn from_hex8(input: &str) -> Result<u32, std::num::ParseIntError> {
    u32::from_str_radix(input, 16)
}

fn parse_hex8(input: &str) -> IResult<&str, u32> {
    map_res(take_while_m_n(8, 8, is_hex_digit), from_hex8)(input)
}

fn parse_doc(input: &str) -> IResult<&str, Doc> {
    let (input, proj) = parse_proj(input)?;
    let (input, value) = parse_hex8(input)?;
    Ok((input, Doc { value, proj }))
}

现在我们需要将self.to_string()str::parse(&str)serde连接起来,我们可以使用一个简单的宏来实现这一点。
macro_rules! serde_str {
    ($type:ty) => {
        impl Serialize for $type {
            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
            where
                S: serde::Serializer,
            {
                let s: String = self.clone().into();
                serializer.serialize_str(&s)
            }
        }

        impl<'de> Deserialize<'de> for $type {
            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
            where
                D: serde::Deserializer<'de>,
            {
                paste! {deserializer.deserialize_string( [<$type Visitor>] {})}
            }
        }

        paste! {struct [<$type Visitor>] {}}

        impl<'de> Visitor<'de> for paste! {[<$type Visitor>]} {
            type Value = $type;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("\"")
            }

            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                match str::parse(v) {
                    Ok(id) => Ok(id),
                    Err(_) => Err(serde::de::Error::custom("invalid format")),
                }
            }
        }
    };
}

这里我们使用 paste 来插值名称。请注意,现在结构体将始终按照上述定义进行序列化。永远不要作为结构体,总是作为字符串。

重要的是要实现 fn visit_str 而不是 fn visit_string,因为 visit_string 推迟到 visit_str

最后,我们必须调用宏来自定义 structs。

serde_str!(Sec);
serde_str!(Doc);
serde_str!(Proj);

现在,指定的类型可以使用serde序列化为字符串并从字符串反序列化。

1
这种解决方案的明显缺点是您必须想出并实现自己的将密钥编码为字符串的方法。一种更可自动化(但诚然不太节省空间)的方法是将密钥编码为转义的JSON字符串,就像这个crate所做的那样:https://github.com/tzcnt/serde_json_any_key/ - HighCommander4
@HighCommander4 感谢您指出这个板条箱。这可能是最省时的方法。 :) - Prophet Lamb

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