System.Text.Json 反序列化失败,并出现 JsonException“读取太多或不够”

GKa*_*kyi 7 c# json .net-core-3.1 system.text.json

此问题适用System.Text.Json于 .Net Core 3.1 中的自定义反序列化类。

我试图理解为什么自定义反序列化类需要读取到 JSON 流的末尾,即使它已经生成了所需的数据,否则反序列化失败并JsonException以“读取太多或不够”结束。

我通读了System.Text.Json([ 1 ], [ 2 ]) 的Microsoft 文档,但无法弄清楚。

这是文档的示例:

{
    "Response": {
        "Result": [
            {
                "Code": "CLF",
                "Id": 49,
                "Type": "H"
            },
            {
                "Code": "CLF",
                "Id": 42,
                "Type": "C"
            }
        ]
    }
}
Run Code Online (Sandbox Code Playgroud)

DTO 类和反序列化方法定义如下:

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);
}
Run Code Online (Sandbox Code Playgroud)

下面是反序列化类的调用方式。

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);
Run Code Online (Sandbox Code Playgroud)

while (reader.Read()) { }反序列化方法中的最后一个循环不存在或被注释掉时,最后一次调用将await JsonSerializer.DeserializeAsync<...失败JsonException,并以 结尾read too much or not enough。谁能解释为什么?或者有没有更好的方法来编写这种反序列化?

更新了第二个代码块以使用EntityDtoIEnumerableConverter.

小智 8

只是提醒任何使用扩展方法或任何外部调用的人,以确保您通过Utf8JsonReader引用传递,否则即使您似乎正确地推进了阅读器,您也可能会遇到一些意外的错误。使用:

public static IReadOnlyDictionary<string, string> ReadObjectDictionary(ref this Utf8JsonReader reader)
Run Code Online (Sandbox Code Playgroud)

...并不是...

public static IReadOnlyDictionary<string, string> ReadObjectDictionary(this Utf8JsonReader reader)
Run Code Online (Sandbox Code Playgroud)


dbc*_*dbc 7

读取对象时,JsonConverter<T>.Read()必须将Utf8JsonReader定位在对象EndObject标记上留下它最初定位的位置。(对于数组,则EndArray是原始数组的 。)在编写Read()解析 JSON 的多个级别的方法时,可以通过CurrentDepth在进入时记住读取器的 ,然后读取直到EndObject在相同深度找到an来完成。

由于您的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
}
Run Code Online (Sandbox Code Playgroud)

笔记:

  • 在您的原始代码中,您在数组反序列化后立即返回。由于JsonSerializer.Deserialize<EntityDto[]>(ref reader, options)仅将读取器推进到嵌套数组的末尾,因此您从未将读取器推进到所需的对象末尾。这导致了您看到的异常。(当当前对象是根对象时,前进到 JSON 流的末尾似乎也有效,但不适用于嵌套对象。)

  • 文档文章中当前没有显示任何转换器如何在您链接的.NET 中编写用于 JSON 序列化(编组)的自定义转换器尝试将多个级别的 JSON 平展为单个 .Net 对象,因此需要跟踪当前的深度似乎没有在实践中出现。

演示小提琴在这里