如何用JsonSerializer.DeserializeAsyncEnumerable流式处理非根级测量数组
流式读取被Header包裹的大型JSON数组(System.Text.Json)
需要用JsonSerializer.DeserializeAsyncEnumerable()流式处理服务器返回的大型JSON响应,但测量数据数组被包裹在根级的Header对象中,结构定义如下:
public record Header(string Id, Measurement[] Measurements); public record Measurement(string Timestamp, decimal Value);
JsonSerializer.DeserializeAsyncEnumerable()仅支持根级为数组的场景,且无需Header内容,目标是避免加载整个数据集到内存(比如计算测量值平均值)。之前尝试自定义JsonConverter和将Header的Measurements属性改为IAsyncEnumerable<Measurement>均未解决问题。
解决方案
可以通过Utf8JsonReader先定位到Measurements数组的起始位置,再逐个流式反序列化数组中的元素,完全避免加载整个数组到内存。具体步骤如下:
1. 异步获取响应流
使用HttpCompletionOption.ResponseHeadersRead确保只读取响应头,响应体留存在流中,不提前加载到内存:
var httpClient = new HttpClient(); using var response = await httpClient.GetAsync("目标API地址", HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); await using var stream = await response.Content.ReadAsStreamAsync();
2. 用Utf8JsonReader定位到Measurements数组
遍历根Header对象的属性,跳过不需要的内容,直到找到"Measurements"对应的数组起始令牌:
var reader = new Utf8JsonReader(stream, new JsonReaderOptions { AllowTrailingCommas = true }); // 跳过根对象的StartObject令牌 await reader.ReadAsync(); if (reader.TokenType != JsonTokenType.StartObject) throw new InvalidOperationException("响应根节点不是预期的对象类型"); // 遍历根对象属性,寻找Measurements数组 while (await reader.ReadAsync()) { if (reader.TokenType == JsonTokenType.PropertyName && reader.GetString() == "Measurements") { // 读取Measurements属性的值令牌(应为StartArray) await reader.ReadAsync(); if (reader.TokenType != JsonTokenType.StartArray) throw new InvalidOperationException("Measurements字段不是预期的数组类型"); // 定位完成,开始处理数组元素 break; } // 跳过当前属性的整个值(避免加载到内存) await reader.SkipAsync(); }
3. 流式处理每个Measurement对象
从数组起始位置开始,逐个反序列化Measurement对象并实时处理(比如计算平均值):
decimal totalValue = 0; int measurementCount = 0; while (await reader.ReadAsync()) { // 到达数组末尾,停止处理 if (reader.TokenType == JsonTokenType.EndArray) break; // 反序列化单个Measurement对象 if (reader.TokenType == JsonTokenType.StartObject) { var measurement = await JsonSerializer.DeserializeAsync<Measurement>(ref reader); totalValue += measurement.Value; measurementCount++; } } // 计算并输出平均值 if (measurementCount > 0) { var average = totalValue / measurementCount; Console.WriteLine($"测量值平均值:{average}"); }
为什么之前的尝试失败?
- 自定义JsonConverter:如果没有针对流式场景设计,常规Converter会尝试加载整个数组到内存后再处理,无法实现真正的流式;
- IAsyncEnumerable
属性 :System.Text.Json默认的集合反序列化逻辑会先完整读取整个数组,再生成异步枚举器,因此即使不迭代,所有Measurement对象已被加载到内存。
内容的提问来源于stack exchange,提问作者Erwin van der Valk
相关产品推荐
相关产品推荐

