是否有更简单的方法使用 System.Text.Json 反序列化具有不同元素的数组?

Is there an easier way of deserialising arrays with different elements using System.Text.Json?

我以这种形式从 API 获取数据:

[
  1234,
  {
    "a": [
      [
        "5541.30000",
        "2.50700000",
        "1534614248.456738",
        "r"
      ],
      [
        "5542.50000",
        "0.40100000",
        "1534614248.456738",
        "r"
      ]
    ],
    "c": "974942666"
  },
  "book-25",
  "XBT/USD"
]

此数组数据中字段的顺序决定了它映射到我的 C# class 中的哪个字段。据我所知,这些元素具有不同的类型,因此不能映射到单个列表或数组。我想将其反序列化为以下 class 结构:

    public class MessageResponseBookSnapshot: MessageResponseBase
    {
        public int ChannelID { get; set; }
        public BookSnapshot Book { get; set; }
        public string ChannelName { get; set; }
        public string Pair { get; set; }
    }

    public class BookSnapshot
    {
        public OrderbookLevel[] As { get; set; }
        public OrderbookLevel[] Bs { get; set; }
    }


    public class OrderbookLevel
    {
        public decimal Price { get; set; }
        public decimal Volume { get; set; }
        public decimal Timestamp { get; set; }
    }

请注意 JSON 和 C# classes 之间存在一些差异,这是与我无关的信息,因此我将其丢弃。如果将这些包含在 class 中可以使反序列化更容易,我很乐意这样做。我无法更改接收数据的格式。

问题是我不知道如何让 JSON 序列化程序自动识别映射,例如。数组的第一个元素映射到 ChannelID。相反,我自己实现了序列化,如下所示:

private static MessageResponseBase DeserialiseBookUpdate(string message)
        {
            using (JsonDocument document = JsonDocument.Parse(message))
            {
                var deserialisedMessage = new MessageResponseBookSnapshot();
                JsonElement root = document.RootElement;
                var arrayEnumerator = root.EnumerateArray();
                arrayEnumerator.MoveNext();
                deserialisedMessage.ChannelID = arrayEnumerator.Current.GetInt32();

                var bookSnapshot = new BookSnapshot();
...cut for brevity...
                deserialisedMessage.Book = bookSnapshot;
                return deserialisedMessage;
            }
        }

这很麻烦,理想情况下我会寻找某种可以应用于 class 字段的属性。有没有更好的方法让 JSON 序列化程序可以找到到我的数据结构的映射,而不必自己手动映射它们?

编辑:

我还想强调一些字段没有映射到它们的类型,例如,JSON 包含引号内的十进制数字,在 JSON 中是输入 string 但我在使用 GetString() 后调用 decimal.Parse()。本题不需要映射成小数,字符串即可。

对于更通用的方法,您可以创建一个自定义 JsonConverter,它使用自定义属性来确定应从 JSON 数组转换哪些类型以及应按何种顺序转换属性。

首先,让我们定义我们将使用的属性:

// Identifies types that should be converted from a JSON array
public class FromJsonArrayAttribute : Attribute { }

// Used to choose which order the properties should be converted in
public class PropertyIndexAttribute : Attribute
{
    public int Index { get; set; }
    public PropertyIndexAttribute(int index)
    {
        Index = index;
    }
}

JsonConverter 看起来像下面这样。请注意,这是一个粗略的版本,只是为了处理问题中的示例,但应该适用于大多数地方。但是,如果您需要能够转换的不仅仅是 public 属性,或者如果 null 可以出现在某些地方,您可能想要稍微调整一下。另外,目前的实现只能反序列化,不能序列化,如你所见:

public class ArrayToObjectConverter<T> : JsonConverter<T>
{
    public ArrayToObjectConverter(JsonSerializerOptions options)
    { }

    public override bool CanConvert(Type typeToConvert)
    {
        // The type we're converting to has a [FromJsonArray] attribute
        return Attribute.IsDefined(typeToConvert, typeof(FromJsonArrayAttribute));
    }

    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartArray)
        {
            throw new JsonException($"Expected the start of a JSON array but found TokenType {reader.TokenType}");
        }
    
        var propMap = typeToConvert.GetProperties() // Only public properties
            .Where(prop => Attribute.IsDefined(prop, typeof(PropertyIndexAttribute)))
            .ToDictionary(
                prop => prop.GetCustomAttribute<PropertyIndexAttribute>().Index,
                prop => prop);
        var result = Activator.CreateInstance(typeToConvert);
        
        var index = 0;
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndArray)
            {
                return (T)result;
            }
            else if (propMap.TryGetValue(index, out var prop))
            {
                var value = JsonSerializer.Deserialize(
                    ref reader,
                    prop.PropertyType,
                    options);
                prop.SetValue(result, value);
            }
            else if (reader.TokenType == JsonTokenType.StartObject)
            {
                // Skip this whole object, as the target type
                // has no matching property for this object
                reader.Skip();
            }
            
            index++;
        }
        
        return (T)result;
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
}

因为转换器应该能够处理不止一种类型(它使用泛型类型参数),所以我们需要使用 JsonConverterFactory

public class ArrayToObjectConverterFactory : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert)
    {
        return Attribute.IsDefined(typeToConvert, typeof(FromJsonArrayAttribute));
    }

    public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
    {
        JsonConverter converter = (JsonConverter)Activator.CreateInstance(
            typeof(ArrayToObjectConverter<>).MakeGenericType(
                new Type[] { type }),
            BindingFlags.Instance | BindingFlags.Public,
            binder: null,
            args: new object[] { options },
            culture: null);

        return converter;
    }
}

现在我们需要注释我们的类型:

[FromJsonArray]
public class MessageResponseBookSnapshot
{
    [PropertyIndex(0)]
    public int ChannelID { get; set; }
    [PropertyIndex(1)]
    public BookSnapshot Book { get; set; }
    [PropertyIndex(2)]
    public string ChannelName { get; set; }
    [PropertyIndex(3)]
    public string Pair { get; set; }
}

public class BookSnapshot
{
    [JsonPropertyName("a")]
    public OrderbookLevel[] As { get; set; }
    [JsonPropertyName("b")]
    public OrderbookLevel[] Bs { get; set; }
}

[FromJsonArray]
public class OrderbookLevel
{
    [PropertyIndex(0)]
    public decimal Price { get; set; }
    [PropertyIndex(1)]
    public decimal Volume { get; set; }
    [PropertyIndex(2)]
    public decimal Timestamp { get; set; }
}

作为一点奖励,让我们处理将 strings 转换为 decimals:

public class DecimalConverter : JsonConverter<decimal>
{
    public override bool CanConvert(Type typeToConvert)
    {
        return typeToConvert == typeof(decimal);
    }

    public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => reader.TokenType switch
    {
        JsonTokenType.Number => reader.GetDecimal(),
        JsonTokenType.String => decimal.Parse(reader.GetString()),
        _ => throw new JsonException($"Expected a Number or String but got TokenType {reader.TokenType}")
    };

    public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
}

最后,让我们做一些反序列化:

var options = new JsonSerializerOptions();
options.Converters.Add(new ArrayToObjectConverterFactory());
options.Converters.Add(new DecimalConverter());
var result = JsonSerializer.Deserialize<MessageResponseBookSnapshot>(json, options);

可以找到有关自定义转换器的更多信息 in the documentation

可以在 this fiddle 中找到一个工作示例。