Utf8Json deserialize to type based on marker field

拈花ヽ惹草 提交于 2021-02-18 17:50:46

问题


With Json.NET - Newtonsoft I have successfully used custom contract deserializers and json converter to select the deserializer based on a tag (in the case below ev).

In summary I am looking to achieve the same with Utf8Json, full details below:

// Stocks TRADE:
{
    "ev": "T",              // Event Type
    "sym": "MSFT",          // Symbol Ticker
    "x": "4",               // Exchange ID
    "i": 12345,             // Trade ID
    "z": 3,                 // Tape ( 1=A 2=B 3=C)
    "p": 114.125,           // Price
    "s": 100,               // Trade Size
    "c": [0, 12],           // Trade Conditions
    "t": 1536036818784      // Trade Timestamp ( Unix MS )
}

// Stocks QUOTE:
{
    "ev": "Q",              // Event Type
    "sym": "MSFT",          // Symbol Ticker
    "bx": "4",              // Bix Exchange ID
    "bp": 114.125,          // Bid Price
    "bs": 100,              // Bid Size
    "ax": "7",              // Ask Exchange ID
    "ap": 114.128,          // Ask Price
    "as": 160,              // Ask Size
    "c": 0,                 // Quote Condition
    "t": 1536036818784      // Quote Timestamp ( Unix MS )
}

// Stocks Aggregate:
{
    "ev": "AM",             // Event Type ( A = Second Agg, AM = Minute Agg )
    "sym": "MSFT",          // Symbol Ticker
    "v": 10204,             // Tick Volume
    "av": 200304,           // Accumulated Volume ( Today )
    "op": 114.04,           // Todays official opening price
    "vw": 114.4040,         // VWAP (Volume Weighted Average Price)
    "o": 114.11,            // Tick Open Price
    "c": 114.14,            // Tick Close Price
    "h": 114.19,            // Tick High Price
    "l": 114.09,            // Tick Low Price
    "a": 114.1314,          // Tick Average / VWAP Price
    "s": 1536036818784,     // Tick Start Timestamp ( Unix MS )
    "e": 1536036818784,     // Tick End Timestamp ( Unix MS )
}

And a text stream that can consist of any of the above types:

[{"ev":"A","sym":"DAL","v":1,"av":1, ...snipped...},{"ev":"T","sym":"MSFT","p":114.11,"x":"4","s":67,"t":1586418423607, ...snipped... }]

And deserialize to:

class Message
{
   List<Trade> Trades { get; set; }
   List<Quote> Quotes { get; set; }
   List<Aggregate> Aggs { get; set; }
}

I am currently doing this, which works, but doubt is as performant as direct deserialization into POCO:

var array = JsonSerializer.Deserialize<dynamic>(@"[{""ev"":""A"",""sym"":""AAL"",""v"":1500,""av"":119037385,""op"":12.64,""vw"":13.1,""o"":13.1,""c"":13.1,""h"":13.1,""l"":13.1,""a"":12.6655,""z"":500,""s"":1586472438000,""e"":1586472439000},{""ev"":""A"",""sym"":""AAL"",""v"":6000,""av"":119043385,""op"":12.64,""vw"":13.1,""o"":13.1,""c"":13.1,""h"":13.1,""l"":13.1,""a"":12.6655,""z"":1000,""s"":1586472439000,""e"":1586472440000},{""ev"":""A"",""sym"":""AAL"",""v"":3000,""av"":119046385,""op"":12.64,""vw"":13.11,""o"":13.11,""c"":13.11,""h"":13.11,""l"":13.11,""a"":12.6655,""z"":1000,""s"":1586472440000,""e"":1586472441000}]");

foreach(var item in array)
{
    if(item["ev"]=="A")
    {
        var aggregate = new OpenHighLowClose(
            DateTimeOffset.FromUnixTimeMilliseconds((long)item["s"]),
            (decimal)item["op"],
            (decimal)item["h"],
            (decimal)item["l"],
            (decimal)item["c"],
            (decimal)item["v"]);
    }
    else if(item["ev"=="AM"]) { }
    else if(item["ev" == "T"]) { }
    else if(item["ev" == "Q"]) { }
}

What is the equivalent of json.net's JsonConverter in Utf8Json, so I can switch the deserializer based on the EV field (and associated string value of either T, A, AM or Q)?

Just to add, i am going from JSON to and want to OpenHighLowClose POCO which I will then send out via message pack... is there any way to skip this intermediate step?


回答1:


If you can assume that the ev field always comes first in an object, it's possible to get away with minimum allocations and minimum extra parsing. In netcoreapp3.0 and later, System.Text.Json supports custom type converters, and one can write code like this:

[JsonConverter(typeof(MessageConverter))]
class Message
{
   List<Trade> Trades { get; set; }
   List<Quote> Quotes { get; set; }
   List<Aggregate> Aggs { get; set; }
}

public class MessageConverter : JsonConverter<Message>
{
    public override bool CanConvert(Type typeToConvert) =>
        typeof(Message).IsAssignableFrom(typeToConvert);

    public override Message Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartArray)
            throw new JsonException();

        var message    = new Message();
        message.Trades = new List<Trade>();
        message.Quotes = new List<Quotes>();
        message.Aggs   = new List<Aggs>();

        while(Expect(ref reader, JsonTokenType.StartObject, JsonTokenType.EndArray))
        {
            Expect(ref reader, JsonTokenType.PropertyName);
            var propertyName = reader.GetString();
            if (propertyName != "ev")
                throw new JsonException();

            switch(ExpectString(ref reader))
            {
            default:
                throw new JsonException();
            case "T":
                var trade = new Trade();
                while(Expect(ref reader, JsonTokenType.PropertyName, JsonTokenType.EndObject))
                switch(reader.GetString())
                {
                default:    throw new JsonException();
                case "sym": trade.Symbol = ExpectString (ref reader); break;
                case "p":   trade.Price  = ExpectDecimal(ref reader); break;
                ...
                }
                message.Trades.Add(trade);
                break;
            ...
            }
        }

        return message;
    }

    public override void Write(Utf8JsonWriter writer, Message message, JsonSerializerOptions options) =>
        throw new NotSupportedException();

    private void Expect(ref Utf8JsonReader reader, JsonTokenType t)
    {
        reader.Read();
        if (reader.TokenType != t)
            throw new JsonException();
    }

    private string ExpectString(ref Utf8JsonReader reader)
    {
        Expect(ref reader, JsonTokenType.String);
        return reader.GetString();
    }

    private decimal ExpectDecimal(ref Utf8JsonReader reader)
    {
        Expect(ref reader, JsonTokenType.Number);
        return reader.GetDecimal();
    }

    private bool Expect(ref Utf8JsonReader reader, JsonTokenType a, JsonTokenType b)
    {
        reader.Read();
        if (reader.TokenType == a) return true ;
        if (reader.TokenType == b) return false ;
        throw new JsonException();
    }
}

If for some reason you can't use netcoreapp3.x, then you will have to forgo JsonSerializer for the outer Message and run the code in the Read method directly. If you are willing to spend a bit of cycles to avoid deserializing the inner objects manually, then apply JsonPropertyName attributes on your property names

struct Trade
{
    [JsonPropertyName("sym")]
    public string  Symbol { get; set; }
    [JsonPropertyName("p")]
    public decimal Price { get; set; }
    ...
}

and rewrite the outer loop of the Read method like this:

        while (Expect(ref reader, JsonTokenType.StartObject, JsonTokenType.EndArray))
        {
            // save reader state to peek the object type
            var copy = reader;

            Expect(ref copy, JsonTokenType.PropertyName);
            var propertyName = copy.GetString();
            if (propertyName != "ev")
                throw new JsonException();

            switch(ExpectString(ref copy))
            {
            default:
                throw new JsonException();
            case "T":
                message.Trades.Add(JsonSerializer.Deserialize<Trade>(ref reader));
                break;
            //...
            }
        }



回答2:


What about a message that looks more like this:

class Message
{
   List<IStockItem> Items { get; set; }
}

IStockItem is an interface and the classes of type Trade, Quote and Aggregate inherit from IStockItem.

In your custom JsonConverter's ReadJson, you'll put your conditions for serialization based on the value of ev

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return string.Empty;
        }
        else if (reader.TokenType == JsonToken.String)
        {
            return serializer.Deserialize(reader, objectType);
        }
        else
        {
            JObject obj = JObject.Load(reader);
            if (obj["ev"] != "A")
                return // Logic for A
            else if (obj["ev"] != "T")
                return // Logic for T
            else
                return serializer.Deserialize(reader, objectType);
        }
    }


来源:https://stackoverflow.com/questions/61116919/utf8json-deserialize-to-type-based-on-marker-field

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!