.net 6+ 推荐用 jsonserializer.deserializeasyncenumerable 流式读取大型 json 数组,要求顶层为数组、流支持 seek、指定元素类型;非纯数组或低版本需用 utf8jsonreader 手动定位并逐项解析,注意 bom 处理与边界控制。

用 JsonSerializer.DeserializeAsyncEnumerable 流式读取大型 JSON 数组
这是 .NET 6+ 最直接的解法:不用把整个数组加载进内存,而是按需反序列化每个元素。前提是 JSON 文件结构是顶层为数组([{...}, {...}, ...]),且每个元素结构一致。
关键点:
-
DeserializeAsyncEnumerable要求流必须支持Seek(比如FileStream),不能用已读完的MemoryStream或网络响应流直接传入 - 必须指定元素类型(如
MyRecord),不能用JsonElement—— 它不支持该 API - 底层仍会缓冲部分数据,但内存占用与单个对象大小成正比,而非整个文件
示例:
await using var stream = File.OpenRead("huge.json");
await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<MyRecord>(stream))
{
Process(item); // 每次只 hold 一个对象
}
用 Utf8JsonReader 手动跳过非数组内容并逐项解析
当 JSON 不是纯数组(比如带根对象:{"data":[...]}),或需要兼容 .NET 5 及更早版本时,得手动控制读取流程。核心是跳过外层结构,定位到数组开始位置,再循环解析每个 JSON 对象。
常见错误:
- 误把
JsonReaderState当作可重用状态,实际每次Utf8JsonReader实例只能用一次 - 没处理逗号分隔符或末尾空格,导致下一项读取失败
- 在数组内遇到嵌套对象/数组时,没用
Depth正确配对起止
实操建议:先用 reader.Read() 走到 JsonTokenType.StartArray,然后用 while (reader.TokenType != JsonTokenType.EndArray) 循环,在每次循环开头检查是否为 StartObject,再用 JsonSerializer.Deserialize<t>(ref reader)</t> 解析当前对象。
为什么不用 JsonDocument.Parse 或 JArray.Load
这两个方案都会将整个 JSON 加载为树形结构,内存峰值 ≈ 文件大小 + 对象开销。对 1GB 的 JSON 数组,很容易触发 OutOfMemoryException,尤其在 32 位进程或内存受限容器中。
它们适合的场景很明确:
-
JsonDocument:需要随机访问、多次查询同一份数据,且文件小于 100MB -
JArray.Load(Newtonsoft):遗留项目、需动态 schema 或复杂 LINQ 查询,但同样全量加载
只要目标只是“顺序遍历每个对象并处理”,它们就是过度设计。
文件编码与 BOM 处理容易被忽略
如果 JSON 文件以 UTF-8 BOM(EF BB BF)开头,Utf8JsonReader 会报 JsonException: 'ï' is an invalid start of a value;而 DeserializeAsyncEnumerable 在 .NET 6 中默认不跳过 BOM,需手动处理。
稳妥做法:
- 用
new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan)配合StreamReader检查 BOM 并跳过 - 或直接用
File.ReadAllBytes判断前 3 字节,再构造无 BOM 的ReadOnlySpan<byte></byte>传给Utf8JsonReader - 避免用
File.OpenText()—— 它返回的StreamReader流无法直接用于Utf8JsonReader
流式处理的真正难点不在解析逻辑,而在边界控制和错误恢复 —— 比如某一行 JSON 格式错误时,是跳过该对象继续,还是中断整个流程?这得结合业务容忍度决定。










