Rust serde结合#[serde(untagged)]与#[serde(with)]反序列化枚举异常
问题原因
你遇到的问题是serde untagged枚举的固有特性和自定义反序列化的错误返回逻辑共同导致的:
- 你定义的
ValueType枚举前两个变体I32、F64只能匹配JSON数字类型的输入,遇到JSON字符串类型的输入会直接返回类型不匹配错误,serde会继续尝试下一个变体。 - 当输入为JSON字符串类型(比如你示例中的
"0"),前两个变体匹配失败后会进入Dt变体的反序列化逻辑:- 你自定义的日期反序列化第一步就调用
String::deserialize(deserializer)?,对于字符串类型输入这一步会成功,此时serde判定该变体的类型已经匹配,不会再尝试其他变体。 - 后续日期解析失败时,你通过
serde::de::Error::custom返回自定义错误,serde会判定为匹配变体的内容非法,直接终止整个反序列化流程,不会再回溯尝试其他变体。
- 你自定义的日期反序列化第一步就调用
解决方案
方案1:允许数字变体从字符串解析(推荐,兼容用户灵活输入的需求)
给I32、F64变体添加自定义反序列化逻辑,支持从字符串类型的数字解析,这样字符串形式的数字会优先匹配前两个变体,不会走到日期解析逻辑:
use serde::de::{Deserializer, Error as DeError}; use std::str::FromStr; fn deserialize_i32<'de, D: Deserializer<'de>>(deserializer: D) -> Result<i32, D::Error> { let value = serde_json::Value::deserialize(deserializer)?; match value { serde_json::Value::Number(n) if n.is_i64() => Ok(n.as_i64().unwrap() as i32), serde_json::Value::String(s) => i32::from_str(&s).map_err(DeError::custom), _ => Err(DeError::custom("invalid i32 value")), } } fn deserialize_f64<'de, D: Deserializer<'de>>(deserializer: D) -> Result<f64, D::Error> { let value = serde_json::Value::deserialize(deserializer)?; match value { serde_json::Value::Number(n) if n.is_f64() => Ok(n.as_f64().unwrap()), serde_json::Value::String(s) => f64::from_str(&s).map_err(DeError::custom), _ => Err(DeError::custom("invalid f64 value")), } } // 修改枚举定义 #[derive(Serialize, Deserialize)] #[serde(untagged)] pub enum ValueType { #[serde(deserialize_with = "deserialize_i32")] I32(i32), #[serde(deserialize_with = "deserialize_f64")] F64(f64), #[serde(with = "datetime_handler")] Dt(DateTime<Utc>), }
如果你不想手动写上述逻辑,可以直接使用serde_with库提供的NumberFromString工具,代码更简洁。
方案2:手动实现ValueType的反序列化逻辑
如果需要更灵活的控制匹配规则,可以放弃派生反序列化,手动实现Deserialize trait,完全自定义匹配顺序和逻辑,避免untagged枚举的隐式规则限制:
impl<'de> Deserialize<'de> for ValueType { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { let value = serde_json::Value::deserialize(deserializer)?; // 先尝试匹配数字 if let serde_json::Value::Number(n) = &value { if n.is_i64() { return Ok(ValueType::I32(n.as_i64().unwrap() as i32)); } else if n.is_f64() { return Ok(ValueType::F64(n.as_f64().unwrap())); } } // 再尝试匹配字符串(先试日期,再试字符串转数字) if let serde_json::Value::String(s) = &value { // 先试解析日期 if let Ok(dt) = DateTime::parse_from_rfc3339(s) { return Ok(ValueType::Dt(dt.with_timezone(&Utc))); } if let Ok(dt) = Utc.datetime_from_str(s, "%Y-%m-%d %H:%M:%S") { return Ok(ValueType::Dt(dt.with_timezone(&Utc))); } // 日期解析失败尝试转数字 if let Ok(num) = i32::from_str(s) { return Ok(ValueType::I32(num)); } if let Ok(num) = f64::from_str(s) { return Ok(ValueType::F64(num)); } } Err(serde::de::Error::custom("invalid value type")) } }
采用这个方案你还可以直接删掉原来的datetime_handler模块里的反序列化逻辑,把日期解析逻辑整合到枚举的反序列化里,代码更内聚。
内容的提问来源于stack exchange,提问作者Haifischbecken
相关产品推荐
相关产品推荐

