如何用Serde解析第三方crate中无Deserialize实现的类型?
如何用Serde解析第三方未实现Deserialize的类型?
问题背景
我在开发CLI工具的配置解析器时碰到了限制:根据Rust的孤儿规则,无法直接为第三方crate(比如termcolor)中的Color类型实现Serde的Deserialize trait,但我需要从配置文件中直接解析出HashMap<String, Color>这样的结构。
初始可行方案(解析为String)
先将颜色值解析为String是可以正常编译的,但后续需要手动转换:
use serde::Deserialize; use std::collections::HashMap; #[derive(Debug, Deserialize)] #[allow(dead_code)] struct Config { colors: HashMap<String, String>, } const TOML_DATA: &str = r#" [colors] color1 = "Cyan" color2 = "Green" not_valid_color = "Qwerty" "#; fn main() { let cfg = toml::from_str::<Config>(TOML_DATA); println!("{:#?}", cfg); }
单个字段的解析方案
利用Serde的remote属性可以为单个Color字段实现解析,但这个方案有局限性:要求原枚举的所有变体都公开,且无法直接作用于HashMap这类容器类型:
use serde::Deserialize; use termcolor::Color; #[derive(Debug, Deserialize)] #[serde(remote = "Color")] enum ColorWrapper { Black, Blue, Green, Red, Cyan, Magenta, Yellow, White, Ansi256(u8), Rgb(u8, u8, u8), __Nonexhaustive, } #[derive(Debug, Deserialize)] #[allow(dead_code)] struct Config { #[serde(with = "ColorWrapper")] color: Color, } const TOML_DATA: &str = r#" color = "Cyan" "#; fn main() { let cfg = toml::from_str::<Config>(TOML_DATA); println!("{:#?}", cfg); }
冗长的手动转换方案
通过定义一个中间的RawConfig先解析为String,再手动转换为Color,虽然可行但代码繁琐:
use serde::Deserialize; use std::collections::HashMap; use std::str::FromStr; use termcolor::Color; use thiserror::Error; #[derive(Debug, Deserialize)] struct RawConfig { colors: HashMap<String, String>, } #[derive(Debug)] #[allow(dead_code)] struct Config { colors: HashMap<String, Color>, } #[derive(Debug, Error)] #[error("cannot parse config")] struct ConfigParseError; impl TryInto<Config> for RawConfig { type Error = ConfigParseError; fn try_into(self) -> Result<Config, Self::Error> { let colors = self .colors .iter() .try_fold( HashMap::new(), |mut acc, (key, value)| match Color::from_str(value) { Ok(val) => { acc.insert(String::clone(key), val); Ok(acc) } Err(_) => Err(ConfigParseError), }, )?; Ok(Config { colors }) } } const TOML_DATA: &str = r#" [colors] color1 = "Cyan" color2 = "Green" #not_valid_color = "Qwerty" "#; fn main() { let raw_cfg: RawConfig = toml::from_str(TOML_DATA).expect("cannot parse toml data"); println!("{:#?}", <RawConfig as TryInto<Config>>::try_into(raw_cfg)); }
更优解决方案:自定义透明包装类型
针对Color这类实现了FromStr的类型,可以定义一个透明包装类型,为其实现Deserialize,这样就能直接用于HashMap等容器:
use serde::Deserialize; use std::collections::HashMap; use std::str::FromStr; use termcolor::Color; // 定义透明包装类型 #[derive(Debug, Clone, Copy)] struct ColorSerde(Color); // 为包装类型实现Deserialize impl<'de> Deserialize<'de> for ColorSerde { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; Color::from_str(&s) .map(ColorSerde) .map_err(serde::de::Error::custom) } } // 让包装类型可以自动转换回Color impl From<ColorSerde> for Color { fn from(wrapper: ColorSerde) -> Self { wrapper.0 } } #[derive(Debug, Deserialize)] #[allow(dead_code)] struct Config { // 直接使用包装类型,后续可以通过into()转换为Color colors: HashMap<String, ColorSerde>, } const TOML_DATA: &str = r#" [colors] color1 = "Cyan" color2 = "Green" ansi_color = "Ansi256(123)" rgb_color = "Rgb(255, 0, 0)" "#; fn main() { let cfg = toml::from_str::<Config>(TOML_DATA).unwrap(); println!("Parsed config: {:#?}", cfg); // 转换为HashMap<String, Color> let colors: HashMap<String, Color> = cfg.colors.into_iter().map(|(k, v)| (k, v.into())).collect(); println!("Converted colors: {:#?}", colors); }
方案说明
- 这个包装类型是透明的,不会带来额外开销
- 利用
Color已实现的FromStrtrait来完成字符串到Color的转换 - 可以直接用于
HashMap、Vec等任何需要Deserialize的容器类型 - 转换回原始
Color类型非常方便,通过Fromtrait自动完成
另一种方案:使用serde的deserialize_with属性
如果不想定义包装类型,也可以为容器字段单独指定反序列化函数:
use serde::Deserialize; use std::collections::HashMap; use std::str::FromStr; use termcolor::Color; // 自定义反序列化函数 fn deserialize_color_map<'de, D>(deserializer: D) -> Result<HashMap<String, Color>, D::Error> where D: serde::Deserializer<'de>, { let raw_map: HashMap<String, String> = HashMap::deserialize(deserializer)?; raw_map.into_iter() .map(|(key, val)| { Color::from_str(&val) .map_err(serde::de::Error::custom) .map(|color| (key, color)) }) .collect() } #[derive(Debug, Deserialize)] #[allow(dead_code)] struct Config { #[serde(deserialize_with = "deserialize_color_map")] colors: HashMap<String, Color>, } const TOML_DATA: &str = r#" [colors] color1 = "Cyan" color2 = "Green" ansi_color = "Ansi256(123)" rgb_color = "Rgb(255, 0, 0)" "#; fn main() { let cfg = toml::from_str::<Config>(TOML_DATA).unwrap(); println!("{:#?}", cfg); }
方案说明
- 直接为
HashMap<String, Color>字段指定自定义反序列化函数 - 先解析为
HashMap<String, String>,再逐个转换为Color - 不需要额外定义包装类型,代码更紧凑
内容的提问来源于stack exchange,提问作者bonvolunegeni
相关产品推荐
相关产品推荐

