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