You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Serde序列化/反序列化键为含数据枚举的HashMap

问题描述

需要实现以携带数据的枚举Foo作为键的HashMap与JSON格式的双向序列化/反序列化,示例代码如下:

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

#[derive(Serialize, Deserialize)]
enum Foo {
  A(u32),
  B(u32),
}

struct Bar {
  h: HashMap<Foo, i32>,
}

fn main() {
  let mut bar = Bar { h: HashMap::new() };
  bar.h.insert(Foo::A(0), 1);

  let bar_string = serde_json::to_string(&bar).unwrap();
  let bar_deser: Bar = serde_json::from_str(&bar_string).unwrap();
}

JSON规范强制要求对象键必须为字符串类型,因此需要自定义Foo作为HashMap键时的序列化、反序列化逻辑。已尝试三类方案均编译通过,但运行时触发panic:

  • 自定义实现Serialize与Deserialize trait
  • 添加#[serde(into = "String", try_from = "String")]注解,同时为Foo实现Into<String>与TryFrom<String>转换逻辑
  • 引入serde_with第三方crate实现转换

可行实现方案

之前方案失败的核心原因:直接为Foo实现的序列化逻辑默认输出结构化值(比如{"A":0}形式的JSON对象),并非字符串类型,不符合JSON键的要求;或者未正确给HashMap字段指定键的序列化路径,导致Serde仍走默认结构化序列化逻辑。

以下两种方案均可稳定运行:

方案1:原生Serde实现(无第三方依赖)

核心逻辑:先为Foo实现字符串双向转换,再为Bar中的HashMap字段指定专用的序列化/反序列化逻辑,确保枚举键始终被序列化为字符串。
注意:作为HashMap键的类型必须实现Eq和Hash trait,派生时不可遗漏

use serde::{Serialize, Deserialize, Serializer, Deserializer};
use serde_json;
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;

#[derive(Debug, PartialEq, Eq, Hash)]
enum Foo {
  A(u32),
  B(u32),
}

// 实现Foo到字符串的格式化逻辑
impl fmt::Display for Foo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Foo::A(v) => write!(f, "A({})", v),
            Foo::B(v) => write!(f, "B({})", v),
        }
    }
}

// 实现字符串到Foo的解析逻辑
impl FromStr for Foo {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let Some((variant, val_part)) = s.split_once('(') else {
            return Err("invalid Foo format".into());
        };
        let Some(val_str) = val_part.strip_suffix(')') else {
            return Err("invalid Foo format".into());
        };
        let val: u32 = val_str.parse().map_err(|e| format!("parse value failed: {}", e))?;
        match variant {
            "A" => Ok(Foo::A(val)),
            "B" => Ok(Foo::B(val)),
            _ => Err(format!("unknown variant: {}", variant)),
        }
    }
}

// HashMap自定义序列化逻辑
mod foo_map_serde {
    use super::Foo;
    use serde::{Serialize, Deserialize, Serializer, Deserializer};
    use std::collections::HashMap;
    use std::str::FromStr;

    pub fn serialize<S: Serializer>(map: &HashMap<Foo, i32>, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.collect_map(map.iter().map(|(k, v)| (k.to_string(), v)))
    }

    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<HashMap<Foo, i32>, D::Error> {
        let str_map: HashMap<String, i32> = HashMap::deserialize(deserializer)?;
        str_map.into_iter()
            .map(|(k_str, v)| {
                Foo::from_str(&k_str)
                    .map(|k| (k, v))
                    .map_err(serde::de::Error::custom)
            })
            .collect()
    }
}

#[derive(Serialize, Deserialize, Debug)]
struct Bar {
  #[serde(with = "foo_map_serde")]
  h: HashMap<Foo, i32>,
}

fn main() {
  let mut bar = Bar { h: HashMap::new() };
  bar.h.insert(Foo::A(0), 1);
  bar.h.insert(Foo::B(123), 2);

  let bar_string = serde_json::to_string(&bar).unwrap();
  // 序列化输出: {"h":{"A(0)":1,"B(123)":2}},完全符合JSON规范
  let bar_deser: Bar = serde_json::from_str(&bar_string).unwrap();
}

你可以根据业务需求修改Display和FromStr的实现,自定义枚举转字符串的格式(比如用A:0、A/0等分隔规则,只要保证无解析歧义即可)。

方案2:serde_with简化实现

如果不想手写HashMap的序列化逻辑,可以用serde_with减少样板代码,核心逻辑和原生实现完全一致:

  1. 添加依赖:serde_with = { version = "3.0", features = ["json"] }
  2. 代码实现:
use serde::{Serialize, Deserialize};
use serde_with::{serde_as, DisplayFromStr};
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;

#[derive(Debug, PartialEq, Eq, Hash)]
enum Foo {
  A(u32),
  B(u32),
}

// 此处Display、FromStr实现和方案1完全一致
impl fmt::Display for Foo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Foo::A(v) => write!(f, "A({})", v),
            Foo::B(v) => write!(f, "B({})", v),
        }
    }
}

impl FromStr for Foo {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let Some((variant, val_part)) = s.split_once('(') else {
            return Err("invalid Foo format".into());
        };
        let Some(val_str) = val_part.strip_suffix(')') else {
            return Err("invalid Foo format".into());
        };
        let val: u32 = val_str.parse().map_err(|e| format!("parse value failed: {}", e))?;
        match variant {
            "A" => Ok(Foo::A(val)),
            "B" => Ok(Foo::B(val)),
            _ => Err(format!("unknown variant: {}", variant)),
        }
    }
}

#[serde_as]
#[derive(Serialize, Deserialize, Debug)]
struct Bar {
  #[serde_as(as = "HashMap<DisplayFromStr, _>")]
  h: HashMap<Foo, i32>,
}

为什么Serde/serde_json不提供默认实现

没有默认实现主要有三个核心原因:

  • 无统一格式标准:带数据的枚举转字符串没有通用的、被广泛认可的格式规范,不同业务场景可能需要完全不同的字符串表示(比如和外部系统交互时可能要求用type=A,value=0的格式),硬编码默认格式会导致序列化结果兼容性差。
  • 避免不必要的性能开销:Serde是通用序列化框架,除了JSON外还支持bincode等二进制序列化后端,这类后端本身支持结构化类型作为键,不需要转字符串。如果默认实现枚举转字符串逻辑,会给非JSON场景增加无意义的字符串分配、解析开销。
  • 规避歧义风险:枚举可以嵌套复杂类型(比如嵌套枚举、带特殊字符的字符串、结构体等),自动生成的字符串表示如果没有完善的转义逻辑,很容易出现解析歧义、反序列化失败的问题,这类通用逻辑的维护成本极高,不如交给开发者根据自身业务场景实现无歧义的转换规则。

内容的提问来源于stack exchange,提问作者Corvus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 09:30:54