Here is the endpoint I'm trying to deserialize. The issue is within the bids
, asks
and changes
. How do I handle it? The API says it's array of [price, quantity]
.
Snippet
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public record Depth(
[property: JsonPropertyName("type")] DepthType Type,
[property: JsonPropertyName("pair")] string Pair,
[property: JsonPropertyName("sequence")] long Sequence,
[property: JsonPropertyName("asks")] List<List<string>> Asks,
//[property: JsonPropertyName("bids")] IEnumerable<Level> Bids,
[property: JsonPropertyName("prev_sequence")] long PreviousSequence
//[property: JsonPropertyName("changes")] IEnumerable<Changes> Changes
);
[JsonConverter(typeof(JsonStringEnumMemberConverter))]
public enum DepthType
{
[EnumMember(Value = "snapshot")]
Snapshot,
[EnumMember(Value = "update")]
Update
}
public record Level(decimal Price, decimal Quantity);
public record Changes(string Side, decimal Price, decimal Quantity);
Response
{
"channel":"depth",
"timestamp":1587929552250,
"module":"spot",
"data":{
"type":"snapshot",
"pair":"BTC-USDT",
"sequence":9,
"bids":[
[
"0.08000000",
"0.10000000"
]
],
"asks":[
[
"0.09000000",
"0.20000000"
]
]
}
}
{
"channel":"depth",
"timestamp":1587930311331,
"module":"spot",
"data":{
"type":"update",
"pair":"BTC-USDT",
"sequence":10,
"prev_sequence":9,
"changes":[
[
"sell",
"0.08500000",
"0.10000000"
]
]
}
}
Attempt
[AttributeUsage(AttributeTargets.Property)]
public sealed class JsonPlainArrayIndexAttribute : Attribute
{
public JsonPlainArrayIndexAttribute(int index)
{
Index = index;
}
public int Index { get; }
}
public sealed class JsonPlainArrayConverter<T> : JsonConverter<T> where T : new()
{
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Debug.Assert(typeof(T) == typeToConvert);
var props = typeToConvert.GetProperties();
var linq = from prop in props
let attr = prop.GetCustomAttributes(typeof(JsonPlainArrayIndexAttribute), true)
where prop.CanWrite && attr.Length is 1
orderby ((JsonPlainArrayIndexAttribute)attr[0]).Index
select prop;
var arr = JsonSerializer.Deserialize<IEnumerable<JsonElement>>(ref reader, options);
if (arr is null)
{
return default;
}
var result = new T();
foreach (var (prop, value) in linq.Zip(arr))
{
prop.SetValue(result, value.Deserialize(prop.PropertyType, options));
}
return result;
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
var type = typeof(T);
var props = type.GetProperties();
var linq = from prop in props
let attr = prop.GetCustomAttributes(typeof(JsonPlainArrayIndexAttribute), true)
where prop.CanRead && attr.Length is 1
orderby ((JsonPlainArrayIndexAttribute)attr[0]).Index
select prop.GetValue(value);
JsonSerializer.Serialize<IEnumerable<object>>(writer, linq, options);
}
}
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public record Depth(
[property: JsonPropertyName("type")] DepthType Type,
[property: JsonPropertyName("pair")] string Pair,
[property: JsonPropertyName("sequence")] long Sequence,
[property: JsonPropertyName("asks")] BookLevel Asks,
[property: JsonPropertyName("bids")] BookLevel Bids,
[property: JsonPropertyName("prev_sequence")] long PreviousSequence
//[property: JsonPropertyName("changes")] IEnumerable<Changes> Changes
);
[JsonConverter(typeof(JsonStringEnumMemberConverter))]
public enum DepthType
{
[EnumMember(Value = "snapshot")]
Snapshot,
[EnumMember(Value = "update")]
Update
}
[JsonConverter(typeof(JsonPlainArrayConverter<BookLevel>))]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public record BookLevel
{
[JsonPlainArrayIndex(0)]
public string Price { get; init; }
[JsonPlainArrayIndex(1)]
public string Quantity { get; init; }
}
public record Changes(string Side, decimal Price, decimal Quantity);
CodePudding user response:
The converter from this answer by yueyinqiu to System.Text.Json fails to deserialize List<List<object>> basically works as-is to bind your BookLevel
model to an array of JSON values corresponding to its properties. However, you have two problems:
Since
"bids"
and"asks"
are jagged 2d arrays in the JSON, you must declare the corresponding properties to be collections, e.g.:public record Depth( [property: JsonPropertyName("asks")] List<BookLevel> Asks, [property: JsonPropertyName("bids")] List<BookLevel> Bids, );
You have applied both
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)
and[JsonConverter(typeof(JsonPlainArrayConverter<BookLevel>))]
toBookLevel
:[JsonConverter(typeof(JsonPlainArrayConverter<BookLevel>))] [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public record BookLevel { [JsonPlainArrayIndex(0)] public string Price { get; init; } [JsonPlainArrayIndex(1)] public string Quantity { get; init; } }
Turns out, when you do that, System.Text.Json will fail with a mysterious internal
System.NullReferenceException
:System.NullReferenceException: Object reference not set to an instance of an object. at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_NumberHandlingOnPropertyInvalid(JsonPropertyInfo jsonPropertyInfo) at System.Text.Json.Serialization.Metadata.JsonPropertyInfo.DetermineNumberHandlingForTypeInfo(Nullable`1 numberHandling) at System.Text.Json.Serialization.Metadata.JsonPropertyInfo.GetPolicies(Nullable`1 ignoreCondition, Nullable`1 declaringTypeNumberHandling) at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.Initialize(Type parentClassType, Type declaredPropertyType, Type runtimePropertyType, ConverterStrategy runtimeClassType, MemberInfo memberInfo, Boolean isVirtual, JsonConverter converter, Nullable`1 ignoreCondition, Nullable`1 parentTypeNumberHandling, JsonSerializerOptions options) at System.Text.Json.Serialization.Metadata.JsonTypeInfo.CreateProperty(Type declaredPropertyType, Type runtimePropertyType, MemberInfo memberInfo, Type parentClassType, Boolean isVirtual, JsonConverter converter, JsonSerializerOptions options, Nullable`1 parentTypeNumberHandling, Nullable`1 ignoreCondition) at System.Text.Json.Serialization.Metadata.JsonTypeInfo..ctor(Type type, JsonConverter converter, Type runtimeType, JsonSerializerOptions options) at System.Text.Json.JsonSerializerOptions.<InitializeForReflectionSerializer>g__CreateJsonTypeInfo|112_0(Type type, JsonSerializerOptions options) at System.Text.Json.JsonSerializerOptions.GetClassFromContextOrCreate(Type type) at System.Text.Json.JsonSerializerOptions.GetOrAddClass(Type type) at System.Text.Json.Serialization.Metadata.JsonTypeInfo.get_ElementTypeInfo() at System.Text.Json.Serialization.JsonCollectionConverter`2.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, TCollection& value) at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value) at System.Text.Json.Serialization.JsonConverter`1.TryReadAsObject(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state, Object& value) at System.Text.Json.Serialization.Converters.LargeObjectWithParameterizedConstructorConverter`1.ReadAndCacheConstructorArgument(ReadStack& state, Utf8JsonReader& reader, JsonParameterInfo jsonParameterInfo) at System.Text.Json.Serialization.Converters.ObjectWithParameterizedConstructorConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value) at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value) at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state) at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 utf8Json, JsonTypeInfo jsonTypeInfo, Nullable`1 actualByteCount) at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 json, JsonTypeInfo jsonTypeInfo) at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
This looks to be a bug in System.Text.Json itself. You could report it if you want. Even if the two attributes are not intended to work together, System.Text.Json should not throw a
NullReferenceException
.Demo fiddle here: https://dotnetfiddle.net/sFWIJg.
As a workaround to issue #2, you could unseal JsonPlainArrayConverter<T>
to allow subclasses to use customized options when deserializing specific properties, like so:
public sealed class JsonNumberHandlingPlainArrayConverter<T> : JsonPlainArrayConverter<T> where T : new()
{
protected override JsonSerializerOptions CustomizePropertyOptions(PropertyInfo info, JsonSerializerOptions options)
{
var copy = new JsonSerializerOptions(options);
copy.NumberHandling = JsonNumberHandling.AllowReadingFromString;
return copy;
}
}
// JsonPlainArrayIndexAttribute and JsonPlainArrayConverter<T> adapted from this answer https://stackoverflow.com/a/71649515/3744182
// By https://stackoverflow.com/users/15283686/yueyinqiu
// To https://stackoverflow.com/questions/71649101/system-text-json-fails-to-deserialize-listlistobject
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public sealed class JsonPlainArrayIndexAttribute : Attribute
{
readonly int index;
public JsonPlainArrayIndexAttribute(int index)
{
this.index = index;
}
public int Index
{
get { return index; }
}
}
public class JsonPlainArrayConverter<T> : JsonConverter<T> where T : new()
{
protected virtual JsonSerializerOptions CustomizePropertyOptions(PropertyInfo info, JsonSerializerOptions options) => options;
public override T? Read(
ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Debug.Assert(typeof(T) == typeToConvert);
var props = typeToConvert.GetProperties();
var linq = from prop in props
let attr = prop.GetCustomAttributes(typeof(JsonPlainArrayIndexAttribute), true)
where prop.CanWrite && attr.Length is 1
orderby ((JsonPlainArrayIndexAttribute)attr[0]).Index
select prop;
var arr = JsonSerializer.Deserialize<IEnumerable<JsonElement>>(ref reader, options);
if (arr is null)
return default;
var result = new T();
foreach (var (prop, value) in linq.Zip(arr))
{
prop.SetValue(result, value.Deserialize(prop.PropertyType, options));
}
return result;
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
var type = typeof(T);
var props = type.GetProperties();
var linq = from prop in props
let attr = prop.GetCustomAttributes(typeof(JsonPlainArrayIndexAttribute), true)
where prop.CanRead && attr.Length is 1
orderby ((JsonPlainArrayIndexAttribute)attr[0]).Index
select prop.GetValue(value);
JsonSerializer.Serialize<IEnumerable<object>>(writer, linq, options);
}
}
And then modify BookLevel
as follows:
[JsonConverter(typeof(JsonNumberHandlingPlainArrayConverter<BookLevel>))]
public record BookLevel
{
[JsonPlainArrayIndex(0)]
public string Price { get; init; }
[JsonPlainArrayIndex(1)]
public string Quantity { get; init; }
}
Demo fiddle here: https://dotnetfiddle.net/AwtFni