如何通过Rust Serde实现枚举条件扁平化,适配JSON多Body字段场景?
解决Rust Serde中条件扁平化枚举处理TextBody/HtmlBody的问题
你需要处理三种JSON场景:仅含TextBody、仅含HtmlBody、同时含两者,并且希望序列化/反序列化时不生成嵌套的Body字段,而是将这两个字段直接平级放在外层结构中。原方案中试图给枚举变体加#[serde(flatten)]但该属性不支持枚举变体,导致无法实现需求。
解决方案
核心思路是通过中间结构体+自定义Serde序列化/反序列化逻辑,将枚举的三种状态映射到对应的JSON字段组合,同时通过flatten属性实现字段平级输出。
步骤1:定义核心枚举和中间结构体
首先定义代表三种Body状态的枚举,以及用于对接JSON字段的中间结构体:
use serde::{Serialize, Deserialize, Deserializer, Serializer}; use serde_json; // 代表三种Body状态的枚举 #[derive(Debug, Clone, PartialEq)] pub enum Body { Text(String), Html(String), HtmlAndText { html: String, text: String }, } // 中间结构体,对应JSON中的TextBody和HtmlBody字段 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] struct BodyFields { #[serde(rename = "TextBody", skip_serializing_if = "Option::is_none")] text: Option<String>, #[serde(rename = "HtmlBody", skip_serializing_if = "Option::is_none")] html: Option<String>, }
步骤2:实现枚举与中间结构体的转换
实现From trait,完成Body和BodyFields的双向转换:
// 从Body引用转换为BodyFields impl From<&Body> for BodyFields { fn from(body: &Body) -> Self { match body { Body::Text(text) => BodyFields { text: Some(text.clone()), html: None }, Body::Html(html) => BodyFields { text: None, html: Some(html.clone()) }, Body::HtmlAndText { text, html } => BodyFields { text: Some(text.clone()), html: Some(html.clone()) }, } } } // 从Body转换为BodyFields impl From<Body> for BodyFields { fn from(body: Body) -> Self { (&body).into() } }
步骤3:自定义Serde序列化/反序列化逻辑
创建模块封装序列化和反序列化函数,对接Serde框架:
mod body_serde { use super::*; use serde::{Serialize, Deserialize, Deserializer, Serializer}; // 序列化:将Body转换为BodyFields后序列化 pub fn serialize<S>(body: &Body, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { let fields: BodyFields = body.into(); fields.serialize(serializer) } // 反序列化:先解析为BodyFields,再转换为Body枚举 pub fn deserialize<'de, D>(deserializer: D) -> Result<Body, D::Error> where D: Deserializer<'de>, { let fields = BodyFields::deserialize(deserializer)?; match (fields.text, fields.html) { (Some(text), None) => Ok(Body::Text(text)), (None, Some(html)) => Ok(Body::Html(html)), (Some(text), Some(html)) => Ok(Body::HtmlAndText { text, html }), (None, None) => Err(serde::de::Error::custom("必须包含TextBody或HtmlBody字段")), } } }
步骤4:定义外层消息结构体
在包含Body的外层结构体中,使用#[serde(with = "body_serde")]和#[serde(flatten)]属性实现字段平级处理:
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Message { pub Field1: String, #[serde(with = "body_serde")] #[serde(flatten)] pub body: Body, pub Field4: String, }
测试验证
通过以下用例验证功能是否正常:
fn main() { // 测试反序列化仅TextBody的场景 let text_json = r#"{ "Field1": "test_value", "TextBody": "sample text content", "Field4": "another_value" }"#; let text_msg: Message = serde_json::from_str(text_json).unwrap(); assert_eq!(text_msg.body, Body::Text("sample text content".to_string())); // 测试反序列化仅HtmlBody的场景 let html_json = r#"{ "Field1": "test_value", "HtmlBody": "<html><body>Hello World!</body></html>", "Field4": "another_value" }"#; let html_msg: Message = serde_json::from_str(html_json).unwrap(); assert_eq!(html_msg.body, Body::Html("<html><body>Hello World!</body></html>".to_string())); // 测试反序列化同时包含两个字段的场景 let both_json = r#"{ "Field1": "test_value", "TextBody": "sample text", "HtmlBody": "<html><body>Hello!</body></html>", "Field4": "another_value" }"#; let both_msg: Message = serde_json::from_str(both_json).unwrap(); assert_eq!(both_msg.body, Body::HtmlAndText { text: "sample text".to_string(), html: "<html><body>Hello!</body></html>".to_string() }); // 测试序列化场景 let serialized_msg = Message { Field1: "test".to_string(), body: Body::HtmlAndText { text: "serialized text".to_string(), html: "<html>serialized</html>".to_string() }, Field4: "test4".to_string() }; let json_output = serde_json::to_string_pretty(&serialized_msg).unwrap(); println!("序列化后的JSON:\n{}", json_output); // 输出的JSON中不会有"body"字段,而是直接包含TextBody和HtmlBody }
原方案不可行的原因
#[serde(flatten)]仅支持结构体字段,无法作用于枚举变体;- 带
#[serde(untagged)]的枚举会按顺序尝试匹配变体:当JSON同时包含TextBody和HtmlBody时,前两个单字段变体都会因存在额外字段匹配失败,第三个结构体变体由于没有flatten支持,无法平级解析字段,最终导致编译或运行错误。
内容的提问来源于stack exchange,提问作者kingledion
相关产品推荐
相关产品推荐

