是否有更简单的方法使用 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; }
}
作为一点奖励,让我们处理将 string
s 转换为 decimal
s:
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 中找到一个工作示例。
我以这种形式从 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; }
}
作为一点奖励,让我们处理将 string
s 转换为 decimal
s:
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 中找到一个工作示例。