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)