Home > Back-end >  Deserializing an array with System.Text.Json
Deserializing an array with System.Text.Json

Time:06-29

It is possible to deserialize an array with Json.NET. How can I deserialize the following JSON using System.Text.Json?

JSON

[340,{"a":["21040.00000",0,"0.00500000"],"b":["21037.70000",0,"0.49900000"],"c":["21039.00000","0.06009660"],"v":["660.49276224","3641.23932460"],"p":["20783.06665","20853.16080"],"t":[6207,22883],"l":["20500.10000","20500.10000"],"h":["21052.40000","21528.70000"],"o":["20716.30000","21416.30000"]},"ticker","XBT/USD"]

Json.NET way

[JsonConverter(typeof(ArrayConverter))]
public class KrakenSocketEvent<T>
{
    [ArrayProperty(0)]
    public int ChannelId { get; set; }

    [ArrayProperty(1)]
    [JsonConversion]
    public T Data { get; set; } = default!;

    [ArrayProperty(2)]
    public string Topic { get; set; } = string.Empty;

    [ArrayProperty(3)]
    public string Symbol { get; set; } = string.Empty;
}

Edit

What I currently have is the following. The issue is with the typeparam. I'm getting a compile time error.

[JsonConverter(typeof(JsonNumberHandlingPlainArrayConverter<KrakenSocketEvent<T>>))] // Attribute argument cannot use type parameters
public record KrakenSocketEvent<T> where T : new()
{
    [JsonPlainArrayIndex(0)]
    public int ChannelId { get; init; }

    [JsonPlainArrayIndex(1)]
    public T Data { get; init; } = default!;

    [JsonPlainArrayIndex(2)]
    public string Topic { get; init; } = null!;

    [JsonPlainArrayIndex(3)]
    public string Symbol { get; init; } = null!;
}

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

[AttributeUsage(AttributeTargets.Property)]
public sealed class JsonPlainArrayIndexAttribute : Attribute
{
    public JsonPlainArrayIndexAttribute(int index)
    {
        Index = index;
    }

    public int Index { get; }
}

public class JsonPlainArrayConverter<T> : JsonConverter<T> where T : new()
{
    protected virtual JsonSerializerOptions CustomizePropertyOptions(PropertyInfo info, JsonSerializerOptions options)
    {
        return 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);
    }
}

CodePudding user response:

You can't use the generic T in your converter declaration, it's an unknown value. For this scenario, you would have to create a JsonConverterFactory instead so you can determine the type T then instantiate your real converter.

It would be easier to write if you had an interface to detect or some attribute to tag with. Then you could write the factory to expect that. Then in your converter, let the serializer deal with how to handle serialization, just direct it on how to read/write values.

[AttributeUsage(AttributeTargets.Class|AttributeTargets.Struct)]
public sealed class JsonPlainArrayAttribute : Attribute
{
    public JsonPlainArrayAttribute(int length) => Length = length;
    public int Length { get; }
}

[AttributeUsage(AttributeTargets.Property)]
public sealed class JsonPlainArrayIndexAttribute : Attribute
{
    public JsonPlainArrayIndexAttribute(int index) => Index = index;
    public int Index { get; }
}

public class JsonPlainArrayConverter : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert)
    {
        return typeToConvert.GetConstructor(Type.EmptyTypes) != null // has default ctor
            && typeToConvert.GetCustomAttribute<JsonPlainArrayAttribute>() != null;
    }

    public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        var converterType = typeof(_JsonPlainArrayConverter<>).MakeGenericType(typeToConvert)!;
        var length = typeToConvert.GetCustomAttribute<JsonPlainArrayAttribute>()?.Length;
        var properties = typeToConvert.GetProperties()
            .Select((p, i) => (i, prop: p, attr: p.GetCustomAttribute<JsonPlainArrayIndexAttribute>()))
            .Where(x => x.i == x.attr?.Index)
            .OrderBy(x => x.i)
            .Select(x => x.prop)
            .ToArray();
        if (length is null || properties.Length != length)
            throw new Exception("bad configuration");
        return Activator.CreateInstance(converterType, (object)properties) as JsonConverter;
    }

    private class _JsonPlainArrayConverter<T> : JsonConverter<T>
        where T: new()
    {
        private readonly PropertyInfo[] properties;
        public _JsonPlainArrayConverter(PropertyInfo[] properties) => this.properties = properties;
        
        public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var arr = JsonSerializer.Deserialize<JsonElement[]>(ref reader, options);
            if (arr is null)
                return default;
            var result = new T();
            foreach (var x in properties.Zip(arr))
            {
                var value = x.Second.Deserialize(x.First.PropertyType, options);
                x.First.SetValue(result, value);
            }
            return result;
        }

        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
        {
            if (value is null)
            {
                writer.WriteNullValue();
                return;
            }
            writer.WriteStartArray();
            foreach (var prop in properties)
            {
                var val = prop.GetValue(value);
                JsonSerializer.Serialize(writer, val, prop.PropertyType, options);
            }
            writer.WriteEndArray();
        }
    }
}
[JsonPlainArray(4)]
public record KrakenSocketEvent<T> where T : new()
{
    [JsonPlainArrayIndex(0)]
    public int ChannelId { get; init; }

    [JsonPlainArrayIndex(1)]
    public T Data { get; init; } = default!;

    [JsonPlainArrayIndex(2)]
    public string Topic { get; init; } = null!;

    [JsonPlainArrayIndex(3)]
    public string Symbol { get; init; } = null!;
}

Usage:

var options = new JsonSerializerOptions
{
    Converters =
    {
        new JsonPlainArrayConverter(),
    },
};
var obj = JsonSerializer.Deserialize<KrakenSocketEvent<YourType>>(json, options);
  • Related