serde_json下实现JSON数组惰性反序列化的更优方法
问题描述
使用serde_json将超长对象数组反序列化为Vec<T>时,必须预先将整个数组加载到内存,处理耗时较长。希望改为迭代读取数组内的元素,以此避免预加载带来的额外处理开销与内存占用。
目前的实现思路
StreamDeserializer仅能迭代连续排列的自定界JSON类型,无法直接处理带外层方括号、元素分隔逗号的标准JSON数组,因此自行实现了一个自定义结构体,为其实现Read特性,在封装原有Read读取流的基础上,自动剥离JSON外层数组的首尾方括号,以及数组元素间的所有逗号。
举例来说,该读取器会将输入的标准JSON [{"name": "foo"}, {"name": "bar"}, {"name": "baz"}] 转换为连续的JSON片段序列 {"name": "foo"} {"name": "bar"} {"name": "baz"},使其满足StreamDeserializer的输入要求。
以下是完整的实现代码:
use std::io; /// 实现 `Read` 特性,将外层为数组的JSON输入转换为相邻的连续JSON值序列 /// 配合 `serde_json::StreamDeserializer` 完成解析 pub(crate) struct ArrayStreamReader<T> { inner: T, depth: Option<usize>, inside_string: bool, escape_next: bool, } impl<T: io::Read> ArrayStreamReader<T> { pub(crate) fn new_buffered(inner: T) -> io::BufReader<Self> { io::BufReader::new(ArrayStreamReader { inner, depth: None, inside_string: false, escape_next: false, }) } } #[inline] fn do_copy(dst: &mut [u8], src: &[u8], len: usize) { if len == 1 { dst[0] = src[0]; // 避免单字节场景的memcpy开销 } else { dst[..len].copy_from_slice(&src[..len]); } } impl<T: io::Read> io::Read for ArrayStreamReader<T> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { if buf.is_empty() { return Ok(0); } let mut tmp = vec![0u8; buf.len()]; // 当读取缓冲区很小(比如长度为1)时,可能所有字节都被过滤 // 此时循环重试,直到从内层流读到可写入缓冲区的有效字节,或者读到EOF loop { let byte_count = self.inner.read(&mut tmp)?; if byte_count == 0 { return if self.depth.is_some() { Err(io::ErrorKind::UnexpectedEof.into()) } else { Ok(0) }; } let mut tmp_pos = 0; let mut buf_pos = 0; for (i, b) in tmp.iter().cloned().enumerate() { if self.depth.is_none() { match b { b'[' => { tmp_pos = i + 1; self.depth = Some(0); }, b if b.is_ascii_whitespace() => {}, b'\0' => break, _ => return Err(io::ErrorKind::InvalidData.into()), } continue; } if self.inside_string { match b { _ if self.escape_next => self.escape_next = false, b'\\' => self.escape_next = true, b'"' if !self.escape_next => self.inside_string = false, _ => {}, } continue; } let depth = self.depth.unwrap(); match b { b'[' | b'{' => self.depth = Some(depth + 1), b']' | b'}' if depth > 0 => self.depth = Some(depth - 1), b'"' => self.inside_string = true, b'}' if depth == 0 => return Err(io::ErrorKind::InvalidData.into()), b',' | b']' if depth == 0 => { let len = i - tmp_pos; do_copy(&mut buf[buf_pos..], &tmp[tmp_pos..], len); tmp_pos = i + 1; buf_pos += len; // 写入空格作为元素分隔符 buf[buf_pos] = b' '; buf_pos += 1; if b == b']' { // 到达外层数组末尾,重置状态支持后续读取其他数组 self.depth = None; } }, _ => {}, } } if tmp_pos < byte_count { let len = byte_count - tmp_pos; do_copy(&mut buf[buf_pos..], &tmp[tmp_pos..], len); buf_pos += len; } if buf_pos > 0 { // 只要写入了有效字节,就返回实际写入长度,否则继续循环重试 return Ok(buf_pos); } } } }
该读取器的实际使用方式如下:
use std::io; use serde::Deserialize; #[derive(Deserialize)] struct Item { name: String, } fn main() -> io::Result<()> { let json = br#"[{"name": "foo"}, {"name": "bar"}]"#; let wrapped = ArrayStreamReader::new_buffered(&json[..]); let first_item: Item = serde_json::Deserializer::from_reader(wrapped) .into_iter() .next() .unwrap()?; assert_eq!(first_item.name, "foo"); Ok(()) }
最终提问
上述自定义实现的开发成本较高,想必存在更成熟、更简便的实现方案,请问应当如何实现基于serde_json的JSON数组惰性反序列化?
方案解答
完全不需要自行实现自定义Read封装,serde_json原生就支持JSON数组的惰性流式反序列化,全程不需要提前加载全量数组内容。
核心原理
基于Read的serde_json反序列化器本身就是流驱动的:词法分析只会按需读取当前解析位置需要的字节,不会一次性把整个输入流加载到内存。只要不强制将结果反序列化为Vec<T>这类需要全量收集的容器,就可以实现逐元素的惰性读取。
针对外层为数组的JSON结构,serde_json提供了稳定的数组遍历API,会自动识别数组的首尾边界、元素间的分隔逗号,不需要手动修改输入流的内容。
可直接运行的实现代码
use serde::Deserialize; use serde::Deserializer; use std::io; #[derive(Deserialize)] struct Item { name: String, } // 单个元素的处理逻辑 fn process_item(item: Item) { println!("处理元素: {}", item.name); } fn main() -> io::Result<()> { // 可以替换为大文件的文件句柄,包一层BufReader即可 let json = br#"[{"name": "foo"}, {"name": "bar"}, {"name": "baz"}]"#; let mut deserializer = serde_json::Deserializer::from_reader(&json[..]); // 识别外层数组结构 let mut seq_access = deserializer.begin_seq()?; // 惰性逐元素读取,每次仅加载当前元素需要的字节 loop { match seq_access.next_element::<Item>() { Ok(item) => process_item(item), Err(e) if e.is_eof() => break, // 数组正常读取完毕 Err(e) => return Err(e.into()), } } Ok(()) }
方案对比优势
- 代码量从百余行的自定义实现缩减到不到30行,没有额外的维护成本
- 完全复用
serde_json经过多年生产验证的词法分析逻辑,能正确处理字符串内的转义括号、逗号等特殊场景,不会出现自定义实现的解析bug - 内存占用仅和单元素大小、内部读取缓冲区大小相关,和整个数组的总长度完全无关,可以处理GB级别的超大JSON数组,不会因为全量加载导致内存溢出
- 读取大文件时,直接将
std::fs::File包一层io::BufReader传入反序列化器即可,不需要提前将整个文件读入内存。
注意事项
- 不要把输入流提前加载为全量字节数组再传入,否则会丢失惰性读取的优势
- 迭代过程中如果需要提前终止,直接跳出循环即可,不需要额外的流清理逻辑
- 该方案完全兼容嵌套数组结构,按照实际元素类型指定泛型参数即可正确解析。
内容的提问来源于stack exchange,提问作者Michael Morgan

