Search code examples
c#jsonpolymorphismsystem.text.json.net-8.0

Polymorphic Json Serialization/Deserialization with custom Converter broken with .NET 8


I have some classes using polymorphism and I need to send them via an OData Controller to the client. On the client I have added Converters to JsonSerializerOptions in order to determine the correct type to serialize/deserialize these Objects.

On server side the classes are decorated with [JsonDerivedType...] OData stores the Type in @odata.type Property.

The Converter used is a TypeDiscriminatingConverter of Generic type, deriving from JsonConvert, basically reading @odata.type Property and serializing/deserializing into the correct type. It's very similar to the first Code Snippet in https://bengribaudo.com/blog/2022/02/22/6569/recursive-polymorphic-deserialization-with-system-text-json.

I am currently doing the update to .NET 8 and on the client side I am now getting the Error

System.NotSupportedException: The converter for derived type 'xy' does not support metadata writes or reads.
   at System.Text.Json.ThrowHelper.ThrowNotSupportedException_BaseConverterDoesNotSupportMetadata(Type derivedType)
   at System.Text.Json.Serialization.Metadata.PolymorphicTypeResolver..ctor(JsonSerializerOptions options, JsonPolymorphismOptions polymorphismOptions, Type baseType, Boolean converterCanHaveMetadata)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.<EnsureConfigured>g__ConfigureSynchronized|172_0()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.EnsureConfigured()

I did research on the Error and Polymorphism in .NET8 using System.Text.Json, but I could not find good ressources in the Web apart from Microsofts Documentation. Finally I dived into System.Text.Json 8.0.1 sources to understand better where the Error arises from. What I found out is, that in PolymorphicTypeResolver it is a must that the Converter can have MetaData if TypeDiscriminators are used. Well, as I see it, that CanHaveMetadata is an internal sealed Property thats not thought to be used by a custom converter.

What can I do? What am I missing?

Currently doing a minimalistic example... will take a little Hoped I am missing some general point which wouldn't need an example.

Here is the minimalistic console app to reproduce:

// See https://aka.ms/new-console-template for more information
using System.Text.Json;
using System.Text.Json.Serialization;

string derivedjson = @"{""$type"":0,""@odata.type"":""derived"",""BaseName"":""base1"",""Name"":""derived1""}";

JsonSerializerOptions options = new JsonSerializerOptions()
{
    Converters =
    {
        new TypeDiscriminatingConverter<Base>((ref Utf8JsonReader reader) =>
        {
            using var doc = JsonDocument.ParseValue(ref reader);
            var typeDiscriminator = doc.RootElement.GetProperty("@odata.type").GetString();

            if (typeDiscriminator!.Contains("derived"))
            {
                return typeof(Derived);
            }

            throw new JsonException();
        })
    },
    WriteIndented = false,
    ReferenceHandler = ReferenceHandler.IgnoreCycles,
    PropertyNameCaseInsensitive = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

var deserialized = JsonSerializer.Deserialize<Base>(derivedjson, options);

public class BaseBase
{
    public string BaseBaseName { get; set; } = "basebase";
}

public enum PublicEnum
{
    PublicEnumValue = 0
}


[JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
[JsonDerivedType(typeof(Derived), typeDiscriminator: nameof(PublicEnum.PublicEnumValue))]
public class Base:BaseBase
{
    public string BaseName { get; set; } = "base";
}

public class Derived:Base
{
    public string Name { get; set; } = "derived";
}

public class TypeDiscriminatingConverter<T> : JsonConverter<T>
{
    public delegate Type TypeDiscriminatorConverter(ref Utf8JsonReader reader);
    private readonly TypeDiscriminatorConverter Converter;

    private Dictionary<Type, string> _typeToPropertyString = new Dictionary<Type, string>
        {
            {typeof(Derived),$"\"$type\":\"0\","},
        };

    public TypeDiscriminatingConverter(TypeDiscriminatorConverter converter) =>
        (Converter) = (converter);

    public override bool CanConvert(Type typeToConvert) =>
        typeToConvert == typeof(T);

    public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var typeCalculatorReader = reader;
        var actualType = Converter(ref typeCalculatorReader);

        return (T?)JsonSerializer.Deserialize(ref reader, actualType, options);
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        if (value is null)
        {
            writer.WriteNullValue();
        }
        else
        {
            var type = value.GetType();

            if (_typeToPropertyString.TryGetValue(type, out string? typeString))
            {
                string temp = JsonSerializer.Serialize(value, type, options);
                temp = temp.Insert(1, typeString);
                writer.WriteRawValue(temp);
            }
            else
            {
                JsonSerializer.Serialize(writer, value, type, options);
            }
        }
    }
}

Solution

  • Since .NET 7 there is no need for extra converter - see the How to serialize properties of derived classes with System.Text.Json doc. Remove it and fix the JsonDerivedTypeAttribute:

    [JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
    [JsonDerivedType(typeof(Derived), (int)PublicEnum.PublicEnumValue)]
    public class Base : BaseBase
    {
        public string BaseName { get; set; } = "base";
    }
    
    JsonSerializerOptions options = new JsonSerializerOptions()
    {
        WriteIndented = false,
        ReferenceHandler = ReferenceHandler.IgnoreCycles,
        PropertyNameCaseInsensitive = true,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    };
    
    // ...
    

    Note that if you can remove the $type property from JSON you can just use @odata.type property as discriminator:

    [JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization, TypeDiscriminatorPropertyName = "@odata.type" )]
    [JsonDerivedType(typeof(Derived), "derived")]
    public class Base : BaseBase
    {
        public string BaseName { get; set; } = "base";
    }
    
    string derivedjson = @"{""@odata.type"":""derived"",""BaseName"":""base1"",""Name"":""derived1""}";