System.Text.Json 反序列化失败并出现 JsonException "read to much or not enough"
System.Text.Json deserialization fails with JsonException "read to much or not enough"
此问题适用于 .Net Core 3.1 中 System.Text.Json
的自定义反序列化 classes。
我试图理解为什么自定义反序列化 class 需要读取到 JSON 流的末尾,即使它已经生成了所需的数据,否则反序列化会失败 JsonException
以 "read too much or not enough."
结尾
我通读了 System.Text.Json
([1], [2]) 的 Microsoft 文档,但无法理解。
这里是文档的一个例子:
{
"Response": {
"Result": [
{
"Code": "CLF",
"Id": 49,
"Type": "H"
},
{
"Code": "CLF",
"Id": 42,
"Type": "C"
}
]
}
}
DTOclass和反序列化方法定义如下:
public class EntityDto
{
public string Code { get; set; }
public int Id { get; set; }
public string Type { get; set; }
}
// This method is a part of class EntityDtoIEnumerableConverter : JsonConverter<IEnumerable<EntityDto>>
public override IEnumerable<EntityDto> Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("JSON payload expected to start with StartObject token.");
}
while ((reader.TokenType != JsonTokenType.StartArray) && reader.Read()) { }
var eodPostions = JsonSerializer.Deserialize<EntityDto[]>(ref reader, options);
// This loop is required to not get JsonException
while (reader.Read()) { }
return new List<EntityDto>(eodPostions);
}
下面是反序列化 class 的调用方式。
var serializerOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
serializerOptions.Converters.Add(new EntityDtoIEnumerableConverter());
HttpResponseMessage message = await httpClient.GetAsync(requestUrl);
message.EnsureSuccessStatusCode();
var contentStream = await msg.Content.ReadAsStreamAsync();
var result = await JsonSerializer.DeserializeAsync<IEnumerable<EntityDto>>(contentStream, serializerOptions);
当反序列化方法中的最后一个循环 while (reader.Read()) { }
不存在或被注释掉时,最后一个调用 await JsonSerializer.DeserializeAsync<...
将失败并返回 JsonException
,并以 read too much or not enough
结束。谁能解释为什么?或者有没有更好的方法来写这个反序列化?
更新第二个代码块使用EntityDtoIEnumerableConverter
。
读取对象时,JsonConverter<T>.Read()
必须将 Utf8JsonReader
保留在对象的 EndObject
标记 最初定位的位置 . (对于数组,原始数组的 EndArray
。)
当编写一个 Read()
方法来解析 JSON 的多个级别时,这可以通过在输入时记住 reader 的 CurrentDepth
来完成,然后读取直到 EndObject
在同一深度被发现。
由于您的 EntityDtoIEnumerableConverter.Read()
方法似乎试图降低 JSON 令牌层次结构,直到遇到数组,然后将数组反序列化为 EntityDto[]
(本质上是剥离"Response"
和 "Result"
包装器属性),您的代码可以重写如下:
public override IEnumerable<EntityDto> Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("JSON payload expected to start with StartObject token.");
}
List<EntityDto> list = null;
var startDepth = reader.CurrentDepth;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == startDepth)
return list;
if (reader.TokenType == JsonTokenType.StartArray)
{
if (list != null)
throw new JsonException("Multiple lists encountered.");
var eodPostions = JsonSerializer.Deserialize<EntityDto[]>(ref reader, options);
(list = new List<EntityDto>(eodPostions.Length)).AddRange(eodPostions);
}
}
throw new JsonException(); // Truncated file or internal error
}
备注:
在您的原始代码中,您在反序列化数组后立即返回。由于 JsonSerializer.Deserialize<EntityDto[]>(ref reader, options)
仅将 reader 推进到嵌套数组的末尾,您从未将 reader 推进到所需的对象末尾。这导致了您看到的异常。 (前进到 JSON 流的末尾似乎也有效 当当前对象是根对象时 ,但不适用于嵌套对象。)
当前在文档文章 How to write custom converters for JSON serialization (marshalling) in .NET 中显示的转换器的 None 您链接到的尝试展平 JSON 像您正在做的那样进入单个 .Net 对象,因此在实践中似乎并没有出现跟踪当前深度的需要。
演示 fiddle here.
提醒任何使用扩展方法或任何外部调用的人,确保您通过引用传递 Utf8JsonReader
,否则您可能会遇到一些意外错误,即使看起来好像您正在推进 reader正确。使用:
public static IReadOnlyDictionary<string, string> ReadObjectDictionary(ref this Utf8JsonReader reader)
...而不是...
public static IReadOnlyDictionary<string, string> ReadObjectDictionary(this Utf8JsonReader reader)
此问题适用于 .Net Core 3.1 中 System.Text.Json
的自定义反序列化 classes。
我试图理解为什么自定义反序列化 class 需要读取到 JSON 流的末尾,即使它已经生成了所需的数据,否则反序列化会失败 JsonException
以 "read too much or not enough."
我通读了 System.Text.Json
([1], [2]) 的 Microsoft 文档,但无法理解。
这里是文档的一个例子:
{
"Response": {
"Result": [
{
"Code": "CLF",
"Id": 49,
"Type": "H"
},
{
"Code": "CLF",
"Id": 42,
"Type": "C"
}
]
}
}
DTOclass和反序列化方法定义如下:
public class EntityDto
{
public string Code { get; set; }
public int Id { get; set; }
public string Type { get; set; }
}
// This method is a part of class EntityDtoIEnumerableConverter : JsonConverter<IEnumerable<EntityDto>>
public override IEnumerable<EntityDto> Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("JSON payload expected to start with StartObject token.");
}
while ((reader.TokenType != JsonTokenType.StartArray) && reader.Read()) { }
var eodPostions = JsonSerializer.Deserialize<EntityDto[]>(ref reader, options);
// This loop is required to not get JsonException
while (reader.Read()) { }
return new List<EntityDto>(eodPostions);
}
下面是反序列化 class 的调用方式。
var serializerOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
serializerOptions.Converters.Add(new EntityDtoIEnumerableConverter());
HttpResponseMessage message = await httpClient.GetAsync(requestUrl);
message.EnsureSuccessStatusCode();
var contentStream = await msg.Content.ReadAsStreamAsync();
var result = await JsonSerializer.DeserializeAsync<IEnumerable<EntityDto>>(contentStream, serializerOptions);
当反序列化方法中的最后一个循环 while (reader.Read()) { }
不存在或被注释掉时,最后一个调用 await JsonSerializer.DeserializeAsync<...
将失败并返回 JsonException
,并以 read too much or not enough
结束。谁能解释为什么?或者有没有更好的方法来写这个反序列化?
更新第二个代码块使用EntityDtoIEnumerableConverter
。
读取对象时,JsonConverter<T>.Read()
必须将 Utf8JsonReader
保留在对象的 EndObject
标记 最初定位的位置 . (对于数组,原始数组的 EndArray
。)
当编写一个 Read()
方法来解析 JSON 的多个级别时,这可以通过在输入时记住 reader 的 CurrentDepth
来完成,然后读取直到 EndObject
在同一深度被发现。
由于您的 EntityDtoIEnumerableConverter.Read()
方法似乎试图降低 JSON 令牌层次结构,直到遇到数组,然后将数组反序列化为 EntityDto[]
(本质上是剥离"Response"
和 "Result"
包装器属性),您的代码可以重写如下:
public override IEnumerable<EntityDto> Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("JSON payload expected to start with StartObject token.");
}
List<EntityDto> list = null;
var startDepth = reader.CurrentDepth;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == startDepth)
return list;
if (reader.TokenType == JsonTokenType.StartArray)
{
if (list != null)
throw new JsonException("Multiple lists encountered.");
var eodPostions = JsonSerializer.Deserialize<EntityDto[]>(ref reader, options);
(list = new List<EntityDto>(eodPostions.Length)).AddRange(eodPostions);
}
}
throw new JsonException(); // Truncated file or internal error
}
备注:
在您的原始代码中,您在反序列化数组后立即返回。由于
JsonSerializer.Deserialize<EntityDto[]>(ref reader, options)
仅将 reader 推进到嵌套数组的末尾,您从未将 reader 推进到所需的对象末尾。这导致了您看到的异常。 (前进到 JSON 流的末尾似乎也有效 当当前对象是根对象时 ,但不适用于嵌套对象。)
当前在文档文章 How to write custom converters for JSON serialization (marshalling) in .NET 中显示的转换器的 None 您链接到的尝试展平 JSON 像您正在做的那样进入单个 .Net 对象,因此在实践中似乎并没有出现跟踪当前深度的需要。
演示 fiddle here.
提醒任何使用扩展方法或任何外部调用的人,确保您通过引用传递 Utf8JsonReader
,否则您可能会遇到一些意外错误,即使看起来好像您正在推进 reader正确。使用:
public static IReadOnlyDictionary<string, string> ReadObjectDictionary(ref this Utf8JsonReader reader)
...而不是...
public static IReadOnlyDictionary<string, string> ReadObjectDictionary(this Utf8JsonReader reader)