【问题标题】:Using serde to deserialize a HashMap with a Enum key使用 serde 反序列化带有 Enum 键的 HashMap
【发布时间】:2021-07-29 16:38:07
【问题描述】:

我有以下 Rust 代码,它模拟了一个配置文件,其中包括一个 HashMap 与一个 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),
    }
}

这是 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"] } }

如果我将 sources HashMap 更改为键入 String 而不是 Source,则 JSON 和 Toml 都将正确反序列化。

我对 serde 和 toml 还很陌生,所以我正在寻找有关如何正确反序列化 toml 变体的建议。

【问题讨论】:

  • TOML 格式仅支持字符串作为键。正因为如此,toml-rs 库仅支持字符串键,但您有一个 Source 枚举作为键,这不是有效的 TOML:github.com/alexcrichton/toml-rs/issues/212
  • @SvetlinZarev 我发现不支持这种用法令人失望,因为 JSON 键也是按定义定义的字符串并且它们能够支持枚举。至少在那个问题中提供了一个合理的解决方法。

标签: rust serde toml


【解决方案1】:

正如其他人在comments 中所说,Toml 解串器doesn't support enums as keys

您可以先使用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 issue 中的建议,保持 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>,
}

这需要SourceFromStr 实现,而不是TryFrom&lt;String&gt;

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&lt;String&gt;,这样序列化也可以工作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-01-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多