0

Pretty straightforward question. Here is a minimal reproducible example which fails to deserialize Side, which is a readonly struct. I want to keep it struct. I know it's possible to make it work with an enum.

The JSON value could not be converted to Issue.Side. Path: $.S | LineNumber: 0 | BytePositionInLine: 107.

{"e":"executionReport","E":1656055862174,"s":"BTCUSDT","c":"electron_e18ae830a5eb47cc86fb3c64499","S":"BUY","o":"LIMIT","f":"GTC","q":"0.00105000","p":"19000.00000000","P":"0.00000000","F":"0.00000000","g":-1,"C":"","x":"NEW","X":"NEW","r":"NONE","i":11208653996,"l":"0.00000000","z":"0.00000000","L":"0.00000000","n":"0","N":null,"T":1656055862174,"t":-1,"I":23815394070,"w":true,"m":false,"M":false,"O":1656055862174,"Z":"0.00000000","Y":"0.00000000","Q":"0.00000000"}
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Issue;

public readonly struct Side
{
    private Side(string value)
    {
        Value = value;
    }

    public static Side Buy => new("BUY");
    public static Side Sell => new("SELL");

    public string Value { get; }

    public static implicit operator string(Side enm)
    {
        return enm.Value;
    }

    public override string ToString()
    {
        return Value;
    }
}

public record BinanceStreamOrderUpdate
{
    [JsonPropertyName("S")] public Side Side { get; init; }

    [JsonPropertyName("q")] public decimal Quantity { get; init; }
}

public class Program
{
    private static void Main()
    {
        var serializerOptions = new JsonSerializerOptions
        {
            NumberHandling = JsonNumberHandling.AllowReadingFromString
        };

        var json = "{\"e\":\"executionReport\",\"E\":1656055862174,\"s\":\"BTCUSDT\",\"c\":\"electron_e18ae830a5eb47cc86fb3c64499\",\"S\":\"BUY\",\"o\":\"LIMIT\",\"f\":\"GTC\",\"q\":\"0.00105000\",\"p\":\"19000.00000000\",\"P\":\"0.00000000\",\"F\":\"0.00000000\",\"g\":-1,\"C\":\"\",\"x\":\"NEW\",\"X\":\"NEW\",\"r\":\"NONE\",\"i\":11208653996,\"l\":\"0.00000000\",\"z\":\"0.00000000\",\"L\":\"0.00000000\",\"n\":\"0\",\"N\":null,\"T\":1656055862174,\"t\":-1,\"I\":23815394070,\"w\":true,\"m\":false,\"M\":false,\"O\":1656055862174,\"Z\":\"0.00000000\",\"Y\":\"0.00000000\",\"Q\":\"0.00000000\"}}";
        var deserialized = JsonSerializer.Deserialize<BinanceStreamOrderUpdate>(json, serializerOptions);

        Console.ReadLine();
    }
}

Example how people did it using Json.NET

internal class OrderSideConverter : BaseConverter<OrderSide>
{
    public OrderSideConverter(): this(true) { }
    public OrderSideConverter(bool quotes) : base(quotes) { }

    protected override List<KeyValuePair<OrderSide, string>> Mapping => new List<KeyValuePair<OrderSide, string>>
    {
        new KeyValuePair<OrderSide, string>(OrderSide.Buy, "BUY"),
        new KeyValuePair<OrderSide, string>(OrderSide.Sell, "SELL")
    };
}

public abstract class BaseConverter<T>: JsonConverter where T: struct
{
    protected abstract List<KeyValuePair<T, string>> Mapping { get; }
    private readonly bool quotes;
    
    protected BaseConverter(bool useQuotes)
    {
        quotes = useQuotes;
    }

    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
    {
        var stringValue = value == null? null: GetValue((T) value);
        if (quotes)
            writer.WriteValue(stringValue);
        else
            writer.WriteRawValue(stringValue);
    }

    public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
    {
        if (reader.Value == null)
            return null;

        var stringValue = reader.Value.ToString();
        if (string.IsNullOrWhiteSpace(stringValue))
            return null;

        if (!GetValue(stringValue, out var result))
        {
            Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {typeof(T)}, Value: {reader.Value}, Known values: {string.Join(", ", Mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo");
            return null;
        }

        return result;
    }

    public T ReadString(string data)
    {
        return Mapping.FirstOrDefault(v => v.Value == data).Key;
    }

    public override bool CanConvert(Type objectType)
    {
        // Check if it is type, or nullable of type
        return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T);
    }

    private bool GetValue(string value, out T result)
    {
        // Check for exact match first, then if not found fallback to a case insensitive match 
        var mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
        if(mapping.Equals(default(KeyValuePair<T, string>)))
            mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));

        if (!mapping.Equals(default(KeyValuePair<T, string>)))
        {
            result = mapping.Key;
            return true;
        }

        result = default;
        return false;
    }

    private string GetValue(T value)
    {
        return Mapping.FirstOrDefault(v => v.Key.Equals(value)).Value;
    }
}
nop
  • 4,711
  • 6
  • 32
  • 93
  • 1
    How are you *expecting* System.Text.Json to deserialize that? (It would really help if you'd reduce this to a minimal example. The majority of the code here seems to be irrelevant to your question.) Have you tried creating a `JsonConverter` for this? – Jon Skeet Jun 24 '22 at 07:43
  • You can look at https://stackoverflow.com/questions/2441290/javascriptserializer-json-serialization-of-enum-as-string for enum deserialization – Davide Vitali Jun 24 '22 at 07:44
  • @DavideVitali, I know that `enum` works. I don't want it to be enum – nop Jun 24 '22 at 07:46
  • @JonSkeet, thanks for your answer. I reduced the code base, or should I remove everything but `Side side`? I haven't tried writing a custom converter. I have a lot of these structs and hopefully the converter could be generic – nop Jun 24 '22 at 07:48
  • No, that's fine. I'd suggest also including sample JSON though. Note that your previous code already *had* a custom converter (`MillisecondEpochDateTimeConverter`), so you should be able to use that as a starting point... – Jon Skeet Jun 24 '22 at 08:02
  • why do you have private constructor for Side ? – CodingMytra Jun 24 '22 at 08:04
  • @JonSkeet, the sample JSON is in the code. – nop Jun 24 '22 at 08:06
  • @Nitz, even without it, it won't work. – nop Jun 24 '22 at 08:07
  • @JonSkeet, I added a sample solution using Json.NET if that'd help for the System.Text.Json converter – nop Jun 24 '22 at 08:15
  • So have you tried creating a new converter based on the code for `MillisecondEpochDateTimeConverter`, but creating a `Side` instead? That would be my first port of call... – Jon Skeet Jun 24 '22 at 08:30
  • @JonSkeet, yes and it doesn't work. https://pastebin.com/ELYPxyi4 – nop Jun 24 '22 at 09:32
  • That code is much more relevant to the question than the Json.NET code, and rather than "it doesn't work" you should say exactly what you're observing. You've asked over 300 questions - by now I'd expect you to know what's important in a question. – Jon Skeet Jun 24 '22 at 13:38

0 Answers0