Search code examples
serializationrusthashmapdeserializationserde

How to use serde to serialize/deserialize a HashMap with enum keys that contain data?


I need to serialize and deserialize a HashMap, h, with enum Foo as a key to and from JSON. Foo's variants contain data (here simplified to u32, but actually are enums themselves):

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

#[derive(Serialize, Deserialize)]
enum Foo {
  A(u32),
  B(u32),
}

// Tried several different things here! Just deriving the relevant traits doesn't work.
struct Bar {
  h: HashMap<Foo, i32>, // The i32 value type is arbitrary
}

fn main() {
  let mut bar = Bar { h: HashMap::new() };
  bar.h.insert(Foo::A(0), 1);

  // I want to be able to do this
  let bar_string = serde_json::to_string(&bar).unwrap();
  let bar_deser: Bar = serde_json::from_str(&bar_string).unwrap();
}

Since the JSON specification requires keys to be strings I know I need to customise the way serialization and deserialization are done for Foo when it is a key in h. I've tried the following:

  • Custom implementations of Serialize and Deserialize (e.g. in the accepted answer here)
  • Serde attributes e.g.: #[serde(into = "String", try_from = "String")] + implementing Into<String> for Foo and TryFrom<String> for Foo (described in the answers here)
  • Using the 'serde_with' crate (also described in the answers here).

Unfortunately none of these have worked - all eventually failed with different panics after compiling successfully.

What is a good way to achieve what I want in serde, if there is one? If not, I'd be very grateful for any workaround suggestions.

Bonus question: why doesn't serde/serde_json provide a default serialization to a String + deserialization of an enum like Foo when it is used as a key in a HashMap?


Solution

  • Here is a working solution with serde_as from the serde_with helper crate:

    use serde::{Deserialize, Serialize};
    use serde_json;
    use std::collections::HashMap;
    
    #[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Debug)]
    enum Foo {
        A(u32),
        B(u32),
    }
    
    #[serde_with::serde_as]
    #[derive(Serialize, Deserialize, Debug)]
    struct Bar {
        #[serde_as(as = "HashMap<serde_with::json::JsonString, _>")]
        h: HashMap<Foo, i32>,
    }
    
    fn main() {
        let mut bar = Bar { h: HashMap::new() };
        bar.h.insert(Foo::A(0), 1);
    
        let bar_string = serde_json::to_string(&bar).unwrap();
        let bar_deser: Bar = serde_json::from_str(&bar_string).unwrap();
    
        println!("{:?}", bar);
        println!("{}", bar_string);
        println!("{:?}", bar_deser);
    }
    
    Bar { h: {A(0): 1} }
    {"h":{"{\"A\":0}":1}}
    Bar { h: {A(0): 1} }
    

    Bonus question: why doesn't serde/serde_json provide a default serialization to a String + deserialization of an enum like Foo when it is used as a key in a HashMap?

    If you noticed, most of those problems become runtime errors.

    That is because serde actually does have a representation of a map that works with all Rust values. So serde itself has no power over this problem.

    The problem comes when serde_json tries to feed the map into a json Serializer. And yes, this serializer could indeed convert any key into a JSON string and back if that was implemented.

    I think it's more of a design decision why this isn't done. If JSON only supports string keys, so should the JSON serializer. If the user wants to use other types as keys, he should convert them to strings first as seen in the code example above.