如何用Serde高效反序列化带枚举标记的向量结构JSON?
针对特殊格式JSON的高性能Serde反序列化方案
你的场景是WebSocket传来数组结构的特殊JSON,顶层固定包含ID、数据(可能多个)、类型标记,需要反序列化为枚举,且因类型多、消息量大,担心untagged枚举的性能开销。以下是两种高效替代方案:
方案1:手动实现Deserialize trait(最优性能)
直接控制反序列化流程,先提取类型标记再针对性解析数据,完全避免untagged枚举的逐个尝试开销。
步骤1:定义基础数据结构
use serde::de::{self, Deserialize, Deserializer, SeqAccess, Visitor}; use serde_json::Value; use std::fmt; #[derive(Debug, Deserialize)] struct Plane { speed: u32, altitude: u32, num_passengers: u32, } #[derive(Debug, Deserialize)] struct RecipeItem { num_ingredients: u32, cook_time: String, oven_temp: u32, } // 用包裹Vec的方式处理Recipe的多个数据对象 #[derive(Debug, Deserialize)] struct Recipe(Vec<RecipeItem>); // 枚举包含ID和对应数据 enum Messages { Plane(u32, Plane), Recipe(u32, Recipe), }
步骤2:为Messages手动实现反序列化
impl<'de> Deserialize<'de> for Messages { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { struct MessagesVisitor; impl<'de> Visitor<'de> for MessagesVisitor { type Value = Messages; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("数组格式:[ID, 数据对象/数组, 类型标记]") } fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> where A: SeqAccess<'de>, { // 读取第一个元素:消息ID let id = seq.next_element::<u32>()? .ok_or_else(|| de::Error::invalid_length(0, &self))?; // 收集剩余元素,分离类型标记和数据部分 let mut elements = Vec::new(); while let Some(elem) = seq.next_element::<Value>()? { elements.push(elem); } if elements.is_empty() { return Err(de::Error::invalid_length(1, &self)); } let type_tag = elements.pop().unwrap(); let type_str = type_tag.as_str() .ok_or_else(|| de::Error::custom("类型标记必须是字符串"))?; // 根据类型标记匹配反序列化逻辑 match type_str { "plane" => { if elements.len() != 1 { return Err(de::Error::custom("Plane类型需恰好一个数据对象")); } let plane = serde_json::from_value(elements.remove(0))?; Ok(Messages::Plane(id, plane)) } "recipe" => { let recipe_items = serde_json::from_value(Value::Array(elements))?; Ok(Messages::Recipe(id, Recipe(recipe_items))) } _ => Err(de::Error::custom(format!("未知消息类型:{}", type_str))), } } } deserializer.deserialize_seq(MessagesVisitor) } }
极致性能优化(可选)
如果想完全避免serde_json::Value的中间内存分配,可以直接在序列访问时针对性读取数据,跳过Value转换:
// 以Plane为例,修改visit_seq中的Plane分支逻辑 "plane" => { let plane = seq.next_element::<Plane>()? .ok_or_else(|| de::Error::invalid_length(2, &self))?; let type_tag = seq.next_element::<String>()? .ok_or_else(|| de::Error::invalid_length(3, &self))?; if type_tag != "plane" { return Err(de::Error::custom("Plane消息类型标记不匹配")); } Ok(Messages::Plane(id, plane)) } // Recipe分支则需要循环读取多个RecipeItem,直到遇到类型标记
这种方式性能最优,但需要针对不同消息类型编写不同的序列读取逻辑,代码复杂度略高。
方案2:自定义反序列化辅助函数(简洁性优先)
如果不想手动实现完整的Visitor,可以用serde的deserialize_with属性,先解析为通用数组结构,再转换为目标枚举:
use serde::Deserialize; #[derive(Deserialize)] #[serde(transparent)] struct RawMessage(#[serde(deserialize_with = "parse_raw_message")] Messages); fn parse_raw_message<'de, D>(deserializer: D) -> Result<Messages, D::Error> where D: Deserializer<'de>, { // 核心逻辑与方案1一致:读取数组、提取ID和类型标记、匹配反序列化 // 代码省略,本质是把方案1的逻辑封装成函数 }
这种方式比untagged枚举性能好,但比手动实现Visitor略逊,胜在代码更简洁。
性能对比
untagged枚举:Serde会逐个尝试20种变体的反序列化,最坏情况要尝试20次,高吞吐量下开销显著。- 手动实现
Visitor:仅需一次数组解析,根据类型标记直接反序列化对应数据,时间复杂度为O(n),性能接近单个结构的反序列化。
内容的提问来源于stack exchange,提问作者0C3D
相关产品推荐
相关产品推荐

