【问题标题】:JsonElement GetRawText method throws "Operation is not valid" exception on empty arrayJsonElement GetRawText 方法在空数组上抛出“操作无效”异常
【发布时间】:2021-10-14 14:08:01
【问题描述】:

情况

我在实现 .NET Core 的 System.Text.Json.JsonSerializer 时遇到了问题。 我的应用程序用于获取数据的 API 返回以下格式的 JSON:

{
    "context": "SomeUnusedContextValue",
    "value": [
        {...}
    ]
}

我只关心实际响应,所以我只需要价值项。

我已经编写了以下方法来获取特定属性并将项目反序列化为对象。

 public static async Task<T?> DeserializeResponse<T>(Stream str, string? property, CancellationToken ct)
 {
        JsonDocument jsonDocument = await JsonDocument.ParseAsync(str, default, ct).ConfigureAwait(false);

        if (property is null) // some calls to the API do return data at root-level
        {
            return JsonSerializer.Deserialize<T>(jsonDocument.RootElement.GetRawText());
        }

        if (!jsonDocument.RootElement.TryGetProperty(property, out JsonElement parsed))
            throw new InvalidDataException($"The specified lookup property \"{property}\" could not be found.");

        return JsonSerializer.Deserialize<T>(!typeof(IEnumerable).IsAssignableFrom(typeof(T))
            ? parsed.EnumerateArray().FirstOrDefault().GetRawText()
            : parsed.GetRawText(), new JsonSerializerOptions
            { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault });
}

问题

现在解决我的问题。有时我只需要一个对象,但是即使只有一个结果,API 仍然会返回一个数组。不是问题,因为正如在底部的 return 语句中所见,我将仅枚举数组并获取第一项(或默认的 null)。当返回的数组为空时,这似乎会崩溃,抛出以下异常:

System.InvalidOperationException: Operation is not valid due to the current state of the object.
   at System.Text.Json.JsonElement.GetRawText()
   at BAS.Utilities.Deserializing.ResponseDeserializer.DeserializeResponse[T](Stream str, String property, CancellationToken ct) in C:\dev\bas.api\Modules\BAS.Utilities\Deserializing\ResponseDeserializer.cs:line 40

我要序列化的对象如下:

public class JobFunctionCombination
{
        /// <summary>
        /// Gets or sets the combined identifier of the main function group, the function group and the sub function group.
        /// </summary>
        /// <example>01_0101_010101</example>
        [JsonPropertyName("job_function_combination_id")]
        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
        public string Id { get; set; } = string.Empty;

        /// <summary>
        /// Gets or sets the combined names of the function groups.
        /// </summary>
        /// <example>Management | Human Resources | Finance controller</example>
        [JsonPropertyName("description")]
        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
        public string Description { get; set; } = string.Empty;

        /// <summary>
        /// Gets or sets the identifier of the main function group.
        /// </summary>
        [JsonPropertyName("job_main_function_group_id")]
        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
        public string MainFunctionGroupId { get; set; } = string.Empty;

        /// <summary>
        /// Gets or sets the identifier of the function group.
        /// </summary>
        [JsonPropertyName("job_function_group_id")]
        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
        public string FunctionGroupId { get; set; } = string.Empty;

        /// <summary>
        /// Gets or sets the identifier of the sub function group.
        /// </summary>
        [JsonPropertyName("job_sub_function_group_id")]
        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
        public string SubFunctionGroupId { get; set; } = string.Empty;
}

类型和 JsonPropertyName 属性都与返回的 JSON 匹配。

尝试修复

为了尝试解决这个问题,我尝试了一些修复(其中两个您仍然可以在给定的代码示例中看到)。

  • JsonIgnore 属性添加到类中的属性。
    • 我尝试将条件设置为WhenWritingDefaultWhenWritingNull。似乎都无法解决问题。
  • 在传递给JsonSerializer.DeserialzeJsonSerializerOptions 对象中设置DefaultIgnoreCondition
    • 这里我也试过WhenWritingDefaultWhenWritingNull,也没有成功。
  • 检查JsonElement 中的数组在枚举时是否为空或使用.isNullOrEmpty() 为空。
    • 这确实阻止了异常的发生,但是对我来说这似乎不是一个实际的修复。更像是一种 FlexTape 解决方案,只是取消了异常。

我不确定确切的问题是什么,除了JsonSerializer 显然存在空对象问题这一明确事实。我能做些什么来解决这个问题?

【问题讨论】:

    标签: c# system.text.json jsonserializer jsonelement


    【解决方案1】:

    您的问题在于您致电FirstOrDefault()

    parsed.EnumerateArray().FirstOrDefault()
    

    JsonElement 是一个 struct,所以当数组没有项目时,FirstOrDefault() 将返回一个默认结构——一个用零初始化但没有设置任何属性值的结构。这样的元素不对应任何 JSON 令牌; ValueKind 将具有默认值 JsonValueKind.NoneGetRawText() 将没有要返回的原始文本。在这种情况下,微软选择让GetRawText() 抛出异常而不是返回空字符串。

    为避免此问题,请枚举数组,使用Select() 语句反序列化每个项目,然后在数组为空时执行FirstOrDefault() 以返回default(T),如下所示:

    public static partial class JsonExtensions
    {
        public static async Task<T?> DeserializeResponse<T>(Stream str, string? property, CancellationToken ct = default)
        {
            if (property is null) // some calls to the API do return data at root-level
            {
                return await JsonSerializer.DeserializeAsync<T>(str, cancellationToken: ct).ConfigureAwait(false);
            }
    
            using var jsonDocument = await JsonDocument.ParseAsync(str, default, ct).ConfigureAwait(false);
    
            if (!jsonDocument.RootElement.TryGetProperty(property, out JsonElement parsed))
                throw new InvalidDataException($"The specified lookup property \"{property}\" could not be found.");
    
            return typeof(T).IsSerializedAsArray() 
                ? parsed.Deserialize<T>()
                : parsed.EnumerateArray().Select(i => i.Deserialize<T>()).FirstOrDefault();
        }       
        
        static bool IsSerializedAsArray(this Type type) =>
            type != typeof(string)
            && typeof(IEnumerable).IsAssignableFrom(type)
            && type != typeof(byte []) // byte arrays are serialized as Base64 strings.
            && !type.IsDictionary();
    
        static bool IsDictionary(this Type type) =>
            typeof(IDictionary).IsAssignableFrom(type)
            || type.GetInterfacesAndSelf().Any(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IDictionary<,>));
    
        static IEnumerable<Type> GetInterfacesAndSelf(this Type type) =>
            (type ?? throw new ArgumentNullException()).IsInterface 
            ? new[] { type }.Concat(type.GetInterfaces())
            : type.GetInterfaces();
    }
    

    注意事项:

    演示小提琴here.

    【讨论】:

      猜你喜欢
      • 2014-01-25
      • 2020-11-10
      • 1970-01-01
      • 1970-01-01
      • 2017-05-26
      • 1970-01-01
      • 2010-10-02
      • 2012-11-22
      相关资源
      最近更新 更多