26

I'm serializing a HashMap with serde, like so:

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

HashMap's key order is unspecified, and since the hashing is randomized (see documentation), the keys actually end up coming out in different order between identical runs.

I'd like my HashMap to be serialized in sorted (e.g. alphabetical) key order, so that the serialization is deterministic.

I could use a BTreeMap instead of a HashMap to achieve this, as BTreeMap::keys() returns its keys in sorted order, but I'd rather not change my data structure just to accommodate the serialization logic.

How do I tell serde to sort the HashMap keys before serializing?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Jo Liss
  • 30,333
  • 19
  • 121
  • 170
  • Note: you may be interested in bluss' [OrderMap](https://github.com/bluss/ordermap), a HashMap whose iteration order solely depends on the order in which elements have been inserted and removed. – Matthieu M. Mar 10 '17 at 16:24

2 Answers2

25

Use the serialize_with field attribute:

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

Here, I've chosen to just rebuild an entire BTreeMap from the HashMap and then reuse the existing serialization implementation.

{
  "map": {
    "alpha": "1",
    "beta": "2",
    "gamma": "3"
  }
}
Sridhar Ratnakumar
  • 81,433
  • 63
  • 146
  • 187
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Nice! There's also an alternative here, relying on `serde_json::Value` using `BTreeMap` internally (which has the advantage of working on a generic `T`, rather than hard-coding the type as `HashMap`): https://stackoverflow.com/a/67792465 – Venryx Mar 31 '22 at 14:36
  • 1
    Hmm, for this approach, wouldn't it be better to make the key and value types generic, as with: `fn ordered_map(value: &HashMap, serializer: S)`? – Venryx Mar 31 '22 at 14:46
  • @Venryx I wouldn't say _better_, but certainly more widely applicable, sure. – Shepmaster Apr 01 '22 at 20:26
  • @Venryx I added a generic implementation to this answer, for the benefit of visitors to this post. – Sridhar Ratnakumar Aug 18 '23 at 01:07
2

A slightly more generic way with automatic sorting, one that uses itertools, and one that only relies on the std lib. Try it on 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)
}

Both of the above functions can be used with these structs:

#[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>,
}
Yuri Astrakhan
  • 8,808
  • 6
  • 63
  • 97