Search code examples
rustserde

How to sort HashMap keys when serializing with serde?


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?


Solution

  • 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"
      }
    }