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