使用serde反序列化带有枚举键的HashMap

5
我有以下的Rust代码,用于模拟一个包含以enum为键的HashMap的配置文件。
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),
    }
}

这是Toml格式的表示方式:
name = "big test"
main_source = "foo"

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

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

这是JSON表示:

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

使用 serde_json 反序列化 JSON 完美运行,但使用 toml 反序列化 TOML 会导致错误。

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

如果我将sourcesHashMapSource更改为以String为键,则JSON和Toml都能正确反序列化。
我对serde和toml都不太熟悉,所以我想寻求关于如何正确反序列化toml变体的建议。

TOML格式仅支持字符串作为键。因此,toml-rs库仅支持字符串键,但您的Source枚举作为键是无效的TOML格式:https://github.com/alexcrichton/toml-rs/issues/212 - Svetlin Zarev
1
@SvetlinZarev 我觉得很失望,因为JSON键也是字符串定义的,它们能够支持枚举,但这种用法却没有得到支持。至少在那个问题中提供了一个合理的解决方法。 - kmdreko
1个回答

11

正如其他人在评论中所说,Toml反序列化器不支持将枚举用作键

您可以使用serde属性先将它们转换为String

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

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

然后实现从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),
        }
    }
}

如果您只需要针对此处的HashMap,您也可以遵循Toml问题中的建议,保持“Source”的定义不变,并使用crate serde_with 修改HashMap的序列化方式:
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>,
}

这需要对Source进行FromStr实现,而不是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),
        }
    }
}

谢谢,使用 try_from = "String" 完美地解决了问题。我还添加了 into = "String" 并实现了 Into<String>,以便序列化也能正常工作。 - Rudedog

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接