Home > Blockchain >  System.Text.Json - Polymorphic serialization for property is not working
System.Text.Json - Polymorphic serialization for property is not working

Time:07-01

KrakenSubscribeRequest.Details is expected to be polymorphic, but it isn't and the interval property is left behind. If KrakenSubscribeRequest was polymorphic, it would've worked with the code below, but in this case KrakenSubscribeRequest.Details is.

var request = new KrakenSubscribeRequest("ohlc", 1, "XBT/USD") { Details = new KrakenOhlcSubscriptionDetails(15) };

// Polymorphic serialization https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism
var options = new JsonSerializerOptions
{
    WriteIndented = true
};
var json = JsonSerializer.Serialize(request, request.GetType(), options);

Actual

{
  "event": "subscribe",
  "reqid": 1,
  "pair": [
    "XBT/USD"
  ],
  "subscription": {
    "name": "ohlc"
  }
}

Expected

{
  "event": "subscribe",
  "reqid": 1,
  "pair": [
    "XBT/USD"
  ],
  "subscription": {
    "interval": 15,
    "name": "ohlc"
  }
}

Objects

public record KrakenSubscribeRequest
{
    public KrakenSubscribeRequest(string topic, long requestId, params string[] symbols)
    {
        RequestId = requestId;
        Details = new KrakenSubscriptionDetails(topic);
        if (symbols.Any())
        {
            Symbols = symbols;
        }
    }

    [JsonPropertyName("event")]
    public string Event { get; init; } = "subscribe";

    [JsonPropertyName("reqid")]
    public long RequestId { get; init; }

    [JsonPropertyName("pair")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public string[]? Symbols { get; init; }

    [JsonPropertyName("subscription")]
    public KrakenSubscriptionDetails Details { get; init; }

    [JsonIgnore]
    public int? ChannelId { get; set; }
}

public record KrakenSubscriptionDetails
{
    public KrakenSubscriptionDetails(string topic)
    {
        Topic = topic;
    }

    [JsonPropertyName("name")]
    public string Topic { get; init; }

    [JsonIgnore]
    public virtual string ChannelName => Topic;
}

public record KrakenOhlcSubscriptionDetails : KrakenSubscriptionDetails
{
    public KrakenOhlcSubscriptionDetails(int interval) : base("ohlc")
    {
        Interval = interval;
    }

    [JsonPropertyName("interval")]
    public int Interval { get; init; }

    [JsonIgnore]
    public override string ChannelName => $"ohlc-{Interval}";
}

Edit

This solves the issue but I would like to get a more generic way.

public class KrakenSubscriptionDetailsConverter : JsonConverter<KrakenSubscriptionDetails>
{
    /// <inheritdoc />
    public override bool CanConvert(Type objectType)
    {
        return typeof(KrakenSubscriptionDetails).IsAssignableFrom(objectType);
    }

    /// <inheritdoc />
    public override KrakenSubscriptionDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    /// <inheritdoc />
    public override void Write(Utf8JsonWriter writer, KrakenSubscriptionDetails value, JsonSerializerOptions options)
    {
        JsonSerializer.Serialize(writer, value, value.GetType(), options);
    }
}

CodePudding user response:

A simpler way of doing it that works with any type is to cast the object to System.Object. This makes use of the fact that the runtime type is used when serializing System.Object.

JsonSerializer.Serialize((object)request, options);

However, this will only apply the polymorphic serialization to the root object. If the polymorphic object is not the root and is instead stored as a property of the root, then using a custom converter is useful. In order to make that converter more generic, you can give it a generic type.

class PolymoprphicConverter<T> : JsonConverter<T> {}

The rest stays the same but replacing KrakenSubscriptionDetails with T. When you want to serialize, simply specify the type as the generic

class KrakenSubscriptionDetailsParent
{
    public KrakenSubscriptionDetails Details { get; set; }
}

KrakenSubscriptionDetailsParent parent = ...
options.Converters.Add(new PolymoprphicConverter<KrakenSubscriptionDetails>());
JsonSerializer.Serialize(parent, options);

A downside of this approach is that it does not work well with tree structures where the parent type is or derived from the child type. If you pass the options to JsonSerializer.Serialize, it will try to use the same converter to serialize the object, causing an infinite loop. To prevent this, you must make sure to remove the converter from the options before serializing, preventing the converter from being applied to children.

  • Related