使用serde序列化时如何对HashMap键进行排序?

28

我正在使用serde序列化一个HashMap,如下所示:

#[derive(Serialize, Deserialize)]
struct MyStruct {
    map: HashMap<String, String>
}

HashMap 的键的顺序是不确定的,并且由于哈希是随机的(参见文档),在相同的运行之间,键实际上以不同的顺序出现。

我想让我的HashMap按排序后的(例如按字母顺序)键顺序进行序列化,这样序列化就是确定性的。

我可以使用BTreeMap代替HashMap来实现这一点,因为BTreeMap::keys()以排序顺序返回其键,但我宁愿不更改我的数据结构来适应序列化逻辑。

如何告诉serde在序列化之前对HashMap键进行排序?


注意:您可能会对bluss的OrderMap感兴趣,这是一个哈希映射表,其迭代顺序仅取决于插入和删除元素的顺序。 - Matthieu M.
2个回答

25
使用serialize_with字段属性
use serde::{Deserialize, Serialize, Serializer}; // 1.0.106
use serde_json; // 1.0.52
use std::collections::{BTreeMap, HashMap};

#[derive(Serialize, Deserialize, Default)]
struct MyStruct {
    #[serde(serialize_with = "ordered_map")]
    map: HashMap<String, String>,
}

/// For use with serde's [serialize_with] attribute
fn ordered_map<S, K: Ord + Serialize, V: Serialize>(
    value: &HashMap<K, V>,
    serializer: S,
) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    let ordered: BTreeMap<_, _> = value.iter().collect();
    ordered.serialize(serializer)
}

fn main() {
    let mut m = MyStruct::default();
    m.map.insert("gamma".into(), "3".into());
    m.map.insert("alpha".into(), "1".into());
    m.map.insert("beta".into(), "2".into());

    println!("{}", serde_json::to_string_pretty(&m).unwrap());
}

这里,我选择从HashMap中重新构建整个BTreeMap,然后重用现有的序列化实现。
{
  "map": {
    "alpha": "1",
    "beta": "2",
    "gamma": "3"
  }
}

不错!这里还有另一种选择,它依赖于 serde_json::Value 并在内部使用 BTreeMap(具有在通用的 T 上工作的优点,而不是硬编码为 HashMap 的类型):https://dev59.com/EcDqa4cB1Zd3GeqPn-iA#67792465 - Venryx
1
嗯,对于这种方法,将键和值类型定义为泛型是否更好,例如:fn ordered_map<K: Ord + Serialize, V: Serialize, S: serde::Serializer>(value: &HashMap<K, V>, serializer: S) - Venryx
@Venryx 我不会说“更好”,但肯定更广泛适用。 - Shepmaster
@Venryx 我在这个回答中添加了一个通用的实现,以便让访问此帖子的访客受益。 - Sridhar Ratnakumar

3

使用自动排序的稍微更通用的方法,其中一个使用itertools,另一个仅依赖于标准库。在playground上尝试。

// This requires itertools crate
pub fn sorted_map<S: Serializer, K: Serialize + Ord, V: Serialize>(
    value: &HashMap<K, V>,
    serializer: S,
) -> Result<S::Ok, S::Error> {
    value
        .iter()
        .sorted_by_key(|v| v.0)
        .collect::<BTreeMap<_, _>>()
        .serialize(serializer)
}

// This only uses std
pub fn sorted_map<S: Serializer, K: Serialize + Ord, V: Serialize>(
    value: &HashMap<K, V>,
    serializer: S,
) -> Result<S::Ok, S::Error> {
    let mut items: Vec<(_, _)> = value.iter().collect();
    items.sort_by(|a, b| a.0.cmp(&b.0));
    BTreeMap::from_iter(items).serialize(serializer)
}

以上的两个函数可以与以下结构一起使用:

#[derive(Serialize)]
pub struct Obj1 {
    #[serde(serialize_with = "sorted_map")]
    pub table: HashMap<&'static str, i32>,
}

#[derive(Serialize)]
pub struct Obj2 {
    #[serde(serialize_with = "sorted_map")]
    pub table: HashMap<String, i32>,
}

在标准版本中,你将数据收集到一个 vec 中并进行排序,然后放入 BTreeMap 中,你只需要执行其中一个操作即可。BTreeMap 本身就是自然排序的。 - undefined

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