Using serde to deserialize a HashMap with a Enum key
Asked Answered
R

1

5

I have the following Rust code which models a configuration file which includes a HashMap keyed with an enum.

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

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
enum Source {
    #[serde(rename = "foo")]
    Foo,
    #[serde(rename = "bar")]
    Bar
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct SourceDetails {
    name: String,
    address: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Config {
    name: String,
    main_source: Source,
    sources: HashMap<Source, SourceDetails>,
}

fn main() {
    let config_str = std::fs::read_to_string("testdata.toml").unwrap();
    match toml::from_str::<Config>(&config_str) {
        Ok(config) => println!("toml: {:?}", config),
        Err(err) => eprintln!("toml: {:?}", err),
    }

    let config_str = std::fs::read_to_string("testdata.json").unwrap();
    match serde_json::from_str::<Config>(&config_str) {
        Ok(config) => println!("json: {:?}", config),
        Err(err) => eprintln!("json: {:?}", err),
    }
}

This is the Toml representation:

name = "big test"
main_source = "foo"

[sources]
foo = { name = "fooname", address = "fooaddr" }

[sources.bar]
name = "barname"
address = "baraddr"

This is the JSON representation:

{
  "name": "big test",
  "main_source": "foo",
  "sources": {
    "foo": {
      "name": "fooname",
      "address": "fooaddr"
    },
    "bar": {
      "name": "barname",
      "address": "baraddr"
    }
  }
}

Deserializing the JSON with serde_json works perfectly, but deserializing the Toml with toml gives the error.

Error: Error { inner: ErrorInner { kind: Custom, line: Some(5), col: 0, at: Some(77), message: "invalid type: string \"foo\", expected enum Source", key: ["sources"] } }

If I change the sources HashMap to be keyed on String instead of Source, both the JSON and the Toml deserialize correctly.

I'm pretty new to serde and toml, so I'm looking for suggestions on how to I would properly de-serialize the toml variant.

Redfish answered 29/7, 2021 at 16:38 Comment(2)
The TOML format supports only strings as keys. And because of that the toml-rs library only supports string keys, but your have a Source enum as key, which is not a valid TOML: github.com/alexcrichton/toml-rs/issues/212Hermaphrodite
@SvetlinZarev I find it disappointing that this use isn't supported, considering JSON keys are also strings by-definition and they are able to support enums. At least there's a reasonable workaround provided in that issue.Listed
A
11

As others have said in the comments, the Toml deserializer doesn't support enums as keys.

You can use serde attributes to convert them to String first:

use std::convert::TryFrom;
use std::fmt;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(try_from = "String")]
enum Source {
    Foo,
    Bar
}

And then implement a conversion from String:

struct SourceFromStrError;

impl fmt::Display for SourceFromStrError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str("SourceFromStrError")
    }
}

impl TryFrom<String> for Source {
    type Error = SourceFromStrError;
    fn try_from(s: String) -> Result<Self, Self::Error> {
        match s.as_str() {
            "foo" => Ok(Source::Foo),
            "bar" => Ok(Source::Bar),
            _ => Err(SourceFromStrError),
        }
    }
}

If you only need this for the HashMap in question, you could also follow the suggestion in the Toml issue, which is to keep the definition of Source the same and use the crate, serde_with, to modify how the HashMap is serialized instead:

use serde_with::{serde_as, DisplayFromStr};
use std::collections::HashMap;

#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Config {
    name: String,
    main_source: Source,
    #[serde_as(as = "HashMap<DisplayFromStr, _>")]
    sources: HashMap<Source, SourceDetails>,
}

This requires a FromStr implementation for Source, rather than TryFrom<String>:

impl FromStr for Source {
    type Err = SourceFromStrError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
       match s {
            "foo" => Ok(Source::Foo),
            "bar" => Ok(Source::Bar),
            _ => Err(SourceFromStrError),
        }
    }
}
Agentival answered 29/7, 2021 at 18:6 Comment(1)
Thanks, using try_from = "String" worked perfect. I also added into = "String" and implemented Into<String> so that serializing also works.Redfish

© 2022 - 2024 — McMap. All rights reserved.