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.

Herohtar
  • 5,347
  • 4
  • 31
  • 41
Rudedog
  • 4,323
  • 1
  • 23
  • 34
  • 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: https://github.com/alexcrichton/toml-rs/issues/212 – Svetlin Zarev Jul 29 '21 at 17:33
  • 1
    @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. – kmdreko Jul 29 '21 at 18:00

1 Answers1

10

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),
        }
    }
}
Peter Hall
  • 53,120
  • 14
  • 139
  • 204
  • Thanks, using `try_from = "String"` worked perfect. I also added `into = "String"` and implemented `Into` so that serializing also works. – Rudedog Jul 29 '21 at 21:40