0

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?

tokoloshe
  • 5
  • 1
  • 4
  • Please provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). Your current example has several errors. It is missing a `main`, `serde:json` should be `serde_json`, the `HashMap` `use` statement is missing, a `;` is missing in the first line. – Finomnis Jun 03 '22 at 12:24
  • I have a feeling that solving this will involve writing your own implementation of `Serialize` and `Deserialize` – Finomnis Jun 03 '22 at 12:33
  • Thanks @Finomnis I've updated the code as per your comment. Next time I'll make sure it runs (first time posting here!). – tokoloshe Jun 03 '22 at 12:33
  • In future you can double-check if it runs by implementing it in https://play.rust-lang.org/ ;) – Finomnis Jun 03 '22 at 12:34
  • `main` is missing a `()` – Finomnis Jun 03 '22 at 12:34
  • `Foo` is missing `derive` for `Hash`, `Eq` and `PartialEq` to be storable in a `HashMap`. Please implement your code in https://play.rust-lang.org/ until you get the exact error message you were describing. – Finomnis Jun 03 '22 at 12:44
  • @jonasbb Yes, it would have helped a lot - thanks! Somehow it never found it while searching before. – tokoloshe Jun 03 '22 at 14:31

1 Answers1

2

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.

Finomnis
  • 18,094
  • 1
  • 20
  • 27
  • Thanks very much @Finomnis for the code and the explanation of the 'bonus' question, very much appreciated! – tokoloshe Jun 03 '22 at 14:28