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

问题描述

我有以下 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,Deserialize)]
struct SourceDetails {
    name: String,address: String,}

#[derive(Debug,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: {:?}",Err(err) => eprintln!("json: {:?}",}
}

这是 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 变体的建议。

解决方法

正如其他人在 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 问题中的建议,即保持 Source 的定义相同并使用板条箱,{{ 3}},修改 HashMap 的序列化方式:

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

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

这需要 FromStrSource 实现,而不是 TryFrom<String>

impl FromStr for Source {
    type Err = SourceFromStrError;
    fn from_str(s: &str) -> Result<Self,Self::Err> {
       match s {
            "foo" => Ok(Source::Foo),}
    }
}