反序列化 [price, quantity] 数组

Deserializing array of [price, quantity]

这是 the endpoint 我正在尝试反序列化。问题在 bidsaskschanges 中。我该如何处理? API 表示它是 array of [price, quantity].

片段

[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);

回应

{
    "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"
            ]
        ]
    }
}

尝试

[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);

来自 by yueyinqiu to 的转换器基本上可以 as-is 将您的 BookLevel 模型绑定到与其属性相对应的 JSON 值数组。但是,您有两个问题:

  1. 由于"bids""asks"在JSON中是锯齿状的二维数组,所以必须声明相应的属性为集合,例如:

    public record Depth(
        [property: JsonPropertyName("asks")] List<BookLevel> Asks,
        [property: JsonPropertyName("bids")] List<BookLevel> Bids,
    );
    
  2. 您已将 [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)[JsonConverter(typeof(JsonPlainArrayConverter<BookLevel>))] 应用到 BookLevel:

    [JsonConverter(typeof(JsonPlainArrayConverter<BookLevel>))]
    [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
    public record BookLevel
    {
        [JsonPlainArrayIndex(0)]
        public string Price { get; init; }
    
        [JsonPlainArrayIndex(1)]
        public string Quantity { get; init; }
    }
    

    事实证明,当你这样做时,System.Text.Json 将失败并出现一个神秘的内部 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)
    

    这看起来是 System.Text.Json 本身的错误。如果你愿意,你可以举报。即使这两个属性不打算一起工作,System.Text.Json 也不应该抛出 NullReferenceException.

    演示 fiddle 此处:https://dotnetfiddle.net/sFWIJg.

作为问题 #2 的解决方法,您可以解封 JsonPlainArrayConverter<T> 以允许子类在反序列化特定属性时使用自定义选项,如下所示:

public sealed class JsonNumberHandlingPlainArrayConverter<T> : JsonPlainArrayConverter<T> where T : new()
{
    protected override JsonSerializerOptions CustomizePropertyOptions(PropertyInfo info, JsonSerializerOptions options) =>
        new JsonSerializerOptions(options)
        {
            NumberHandling = JsonNumberHandling.AllowReadingFromString,
        };
}

// JsonPlainArrayIndexAttribute and  JsonPlainArrayConverter<T> adapted from this answer 
// By https://whosebug.com/users/15283686/yueyinqiu
// To 

[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public sealed class JsonPlainArrayIndexAttribute : Attribute
{
    public int Index { get; }
    public JsonPlainArrayIndexAttribute(int index) => this.Index = 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, CustomizePropertyOptions(prop, 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);
    }
}

然后修改BookLevel如下:

[JsonConverter(typeof(JsonNumberHandlingPlainArrayConverter<BookLevel>))]
public record BookLevel
{
    [JsonPlainArrayIndex(0)]
    public decimal Price { get; init; }

    [JsonPlainArrayIndex(1)]
    public decimal Quantity { get; init; }
}

演示 fiddle 此处:https://dotnetfiddle.net/AwtFni