Home > OS >  System.Text.Json fails to deserialize List<List<object>>
System.Text.Json fails to deserialize List<List<object>>

Time:03-29

I'm using System.Text.Json and it fails to deserialize BookLevel[]. BookLevel is something like List<List<object>>.

The JSON value could not be converted to Deribit.Models.BookLevel. Path: $.params.data.bids[0] | LineNumber: 0 | BytePositionInLine: 234.. Exception: JsonException

public record BookResponse
{
    [JsonPropertyName("type")]
    public string Type { get; init; } = null!;

    [JsonPropertyName("timestamp")]
    public long Timestamp { get; init; }

    [JsonPropertyName("prev_change_id")]
    public decimal PreviousChangeId { get; init; }

    [JsonPropertyName("instrument_name")]
    public string InstrumentName { get; init; } = null!;

    [JsonPropertyName("change_id")]
    public decimal ChangeId { get; init; }

    [JsonPropertyName("bids")]
    public BookLevel[] Bids { get; init; } = null!;

    [JsonPropertyName("asks")]
    public BookLevel[] Asks { get; init; } = null!;
}

public record BookLevel
{
    [JsonPropertyOrder(1)]
    public string Action { get; init; } = null!;

    [JsonPropertyOrder(2)]
    public decimal Amount { get; init; }

    [JsonPropertyOrder(3)]
    public decimal Price { get; init; }
}

{"jsonrpc":"2.0","method":"subscription","params":{"channel":"book.BTC-PERPETUAL.100ms","data":{"type":"change","timestamp":1648477437698,"prev_change_id":42599922395,"instrument_name":"BTC-PERPETUAL","change_id":42599922580,"bids":[["change",47452.0,55700.0],["change",47451.5,24170.0],["delete",47449.0,0.0],["new",47446.5,2130.0],["change",47440.5,56210.0],["new",47439.0,46520.0],["new",47438.0,660.0],["new",47437.0,47430.0],["change",47429.5,20000.0],["change",47429.0,2810.0],["change",47428.5,36460.0],["change",47428.0,3070.0],["new",47427.0,21110.0],["delete",47423.5,0.0],["new",47421.0,33400.0],["change",47420.5,33190.0],["new",47420.0,140.0],["change",47390.0,63980.0],["new",47382.0,85480.0],["delete",47381.0,0.0],["new",47379.5,32770.0]],"asks":[["change",47452.5,15950.0],["new",47467.0,101970.0],["delete",47467.5,0.0],["change",47469.0,1200.0],["change",47470.5,31470.0],["change",47471.5,2010.0],["change",47474.0,79380.0],["change",47474.5,47470.0],["new",47475.5,2970.0],["new",47476.0,21010.0],["change",47476.5,7630.0],["change",47477.0,42510.0],["change",47478.5,100.0],["delete",47480.0,0.0],["change",47482.5,5650.0],["delete",47485.5,0.0],["new",47494.0,150.0],["change",47494.5,43340.0],["new",47523.5,32590.0],["delete",47527.5,0.0]]}}}

Back in Newtonsoft.Json

I could've marked BookLevel as [JsonConverter(typeof(ObjectToArrayConverter<LevelEvent>))] and use the following. How do I do that with System.Text.Json?

/// <summary>
/// Adapted from https://stackoverflow.com/questions/39461518/c-sharp-json-net-deserialize-response-that-uses-an-unusual-data-structure
/// </summary>
/// <typeparam name="T"></typeparam>
public class ObjectToArrayConverter<T> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(T) == objectType;
    }

    static bool ShouldSkip(JsonProperty property)
    {
        return property.Ignored || !property.Readable || !property.Writable;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var type = value.GetType();

        if (!(serializer.ContractResolver.ResolveContract(type) is JsonObjectContract contract))
        {
            throw new JsonSerializationException("invalid type "   type.FullName);
        }
    
        var list = contract.Properties.Where(p => !ShouldSkip(p)).Select(p => p.ValueProvider.GetValue(value));
        serializer.Serialize(writer, list);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }
    
        var token = JArray.Load(reader);

        if (!(serializer.ContractResolver.ResolveContract(objectType) is JsonObjectContract contract))
        {
            throw new JsonSerializationException("invalid type "   objectType.FullName);
        }
    
        var value = existingValue ?? contract.DefaultCreator();
    
        foreach (var pair in contract.Properties.Where(p => !ShouldSkip(p)).Zip(token, (p, v) => new { Value = v, Property = p }))
        {
            var propertyValue = pair.Value.ToObject(pair.Property.PropertyType, serializer);
            pair.Property.ValueProvider.SetValue(value, propertyValue);
        }

        return value;
    }
}

CodePudding user response:

For your BookLevel, you can:

using System.Text.Json;
using System.Text.Json.Serialization;

[JsonConverter(typeof(BookLevelConverter))]
public record BookLevel
{
    public string Action { get; init; } = null!;

    public decimal Amount { get; init; }

    public decimal Price { get; init; }
}

public class BookLevelConverter : JsonConverter<BookLevel>
{
    public override BookLevel? Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var arr = JsonSerializer.Deserialize<JsonElement[]>(ref reader, options);
        return arr is null ? null : new BookLevel()
        {
            Action = arr[0].GetString()!,
            Amount = arr[1].GetInt32(),
            Price = arr[2].GetInt32()
        };
    }

    public override void Write(
        Utf8JsonWriter writer, BookLevel value, JsonSerializerOptions options)
    {
        var arr = new object[] { value.Action, value.Amount, value.Price };
        JsonSerializer.Serialize(writer, arr, options);
    }
}

But why are you doing this? It's a response from a third party service?


Well, there is the more generic way. Although I have tested it on your BookLevel, I can't guarantee that it could work on all types and in all edge cases. And I don't know whether there are simpler solutions or not.

All the public properties with JsonPlainArrayIndexAttribute will be serialized. Attributes like JsonIgnore won't take effect. You can ignore a property by not adding the JsonPlainArrayIndexAttribute. And to make sure it can be successfully deserilized, the options like JsonSerializerOptions.DefaultIgnoreCondition will be ignored as well.

using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;


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

And the BookLevel:

[JsonConverter(typeof(JsonPlainArrayConverter<BookLevel>))]
public record BookLevel
{
    [JsonPlainArrayIndex(0)]
    public string Action { get; init; } = null!;

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

    [JsonPlainArrayIndex(2)]
    public decimal Price { get; init; }
}
  • Related