skip to Main Content

First of all, I apologize for the length of below code; it’s about as minimal as I could get it.

I’m trying to (de)serialize a dozen or so messages that are defined in a 3rd party library; I cannot modify these classes and interfaces in any way. The messages are all interface based; they share no baseclass unfortunately, just the IMessage<T> interface. Below are the messages; I’ve left out irrelevant properties like Header on an IMessage<T> etc. I’ve provided two of each message ‘types’, a Foo and a Bar, for demonstration purposes.

// Below types are defined by a 3rd party and cannot be modified
public interface IMessage<out T> where T : IMessageContent
{
    IMessageBody<T> Body { get; }
}

public interface IMessageBody<out T> where T : IMessageContent
{
    T Content { get; }
}

public interface IMessageContent {
    string MessageId { get; set; }
}

public class FooMessage : IMessage<Foo>
{
    public IMessageBody<Foo> Body { get; set; }
}

public class BarMessage : IMessage<Bar>
{
    public IMessageBody<Bar> Body { get; set; }
}

public class FooBody : IMessageBody<Foo>
{
    public Foo Content { get; set; }
}

public class BarBody : IMessageBody<Bar>
{
    public Bar Content { get; set; }
}

public class Foo : IMessageContent
{
    public string MessageId { get; set; }
    public string FooValue { get; set; }
}

public class Bar : IMessageContent
{
    public string MessageId { get; set; }
    public string BarValue { get; set; }
}

Since I cannot modify the above types, I cannot use [JsonDerivedType] attributes and such. I’m hoping Configuring polymorphism with the contract model will be the solution – but it may not be. I prefer to use System.Text.Json – though if absolutely necessary I will consider NewtonSoft.Json.

So here’s what I’m trying to do:

var message1 = new FooMessage { Body = new FooBody { Content = new Foo { MessageId = "abc", FooValue = "foo" } } };
//var message2 = new BarMessage { Body = new BarBody { Content = new Bar { MessageId = "xyz", BarValue = "bar" } } };

var opt = new JsonSerializerOptions
{
    TypeInfoResolver = new MyMessageTypeResolver()
};

// Serialize a message
var json = JsonSerializer.Serialize(message1.Body.Content, opt);
Console.WriteLine(json);
// Deserialize message
var obj = JsonSerializer.Deserialize<IMessageContent>(json, opt);

For this purpose I have implemented a MyMessageTypeResolver which is passed into the serializer’s Serialize(...) and Deserialize(...) methods.

public class MyMessageTypeResolver : DefaultJsonTypeInfoResolver
{
    public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
    {
        var jsonTypeInfo = base.GetTypeInfo(type, options);

        if (typeof(IMessageContent).IsAssignableFrom(jsonTypeInfo.Type))
        {
            jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
            {
                TypeDiscriminatorPropertyName = "$type",
                IgnoreUnrecognizedTypeDiscriminators = true,
                UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
                DerivedTypes =
                {
                    new JsonDerivedType(typeof(Foo), nameof(Foo)),
                    //new JsonDerivedType(typeof(Bar), nameof(Bar)),
                }
            };
        }

        return jsonTypeInfo;
    }
}

The above code works; it can be seen in action in this dotnetfiddle. As you can see, a type discriminator "$type": "Foo" is correctly added to the JSON.

However, the problem begins when I uncomment the new JsonDerivedType(typeof(Bar), nameof(Bar)), line; this results in the following exception:

System.InvalidOperationException: Specified type ‘Bar’ is not a supported derived type for the polymorphic type ‘Foo’. Derived types must be assignable to the base type, must not be generic and cannot be abstract classes or interfaces unless ‘JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor’ is specified.

I’ve tried a lot of variations, but I can’t get this to properly (de)serialize for the life of me. Any help or pointers in the right direction are much appreciated. The goal is to be able to (de)serialize message1 and message2 in above given code. I’m not dead set on using the JsonTypeInfoResolver route; any other way (that’s manageable for a dozen, maybe more in the future, message types) that achieves this goal is welcome.

2

Answers


  1. You can not create a generic custom serializer, not even a generic method because all your generic interfaces have only {get;} (read only). Only concrete clases have setters. So the easiest way is to use the code like this for each of your third party classes

    FooMessage fooMessage = new FooMessage { 
                     Body = JsonObject.Parse(json)["Body"].Deserialize<FooBody>()};
    
    Login or Signup to reply.
  2. Your problem is here:

    if (typeof(IMessageContent).IsAssignableFrom(jsonTypeInfo.Type))
    {
        // Intermediate code snipped
        DerivedTypes = 
        {
            new JsonDerivedType(typeof(Foo), nameof(Foo)),
            new JsonDerivedType(typeof(Bar), nameof(Bar)),
        }
    

    You check to see whether jsonTypeInfo.Type is assignable to IMessageContent, and then add derived types Foo and Bar. But IMessageContent has many derived types, including Foo itself. Then when your code is invoked, it is actually attempting to assign the derived type Bar to Foo, which fails, because Bar is not a derived type of Foo.

    To fix this, you should check that each incoming derived type is actually assignable to JsonTypeInfoInfo.Type before adding it. Here is a typeInfo modifier that does that:

    public class JsonExtensions
    {
        public static Action<JsonTypeInfo> AddDerivedTypes(Type baseType, IEnumerable<Type> derivedTypes) => typeInfo => 
        {
            if (typeInfo.Kind != JsonTypeInfoKind.Object)
                return;
            if (baseType.IsAssignableFrom(typeInfo.Type))
            {
                var applicableDerivedTypes = derivedTypes
                    // Check to make sure the current derived type is actually assignable to typeInfo.Type (which might also be a derived type of baseType)
                    .Where(t => typeInfo.Type.IsAssignableFrom(t));
                bool first = true;
                foreach (var type in applicableDerivedTypes)
                {
                    if (first)
                        // TODO: decide what to do if typeInfo.PolymorphismOptions is not null (because e.g. it was initialized via attributes).
                        typeInfo.PolymorphismOptions = new JsonPolymorphismOptions
                        {
                            TypeDiscriminatorPropertyName = "$type",
                            IgnoreUnrecognizedTypeDiscriminators = true,
                            UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
                        };
                    first = false;
                    typeInfo.PolymorphismOptions!.DerivedTypes.Add(new JsonDerivedType(type, type.Name));
                }
            }
        };
    }
    

    You would then use the modifier as follows:

    var opt = new JsonSerializerOptions
    {
        TypeInfoResolver = new DefaultJsonTypeInfoResolver
        {
            Modifiers = { JsonExtensions.AddDerivedTypes(typeof(IMessageContent), new [] { typeof(Foo), typeof(Bar) }) },
        }
    };
    

    Notes:

    • I recommend customizing System.Text.Json contracts via modifiers rather than by inheriting from DefaultJsonTypeInfoResolver because modifiers can more easily be combined and reused.

    Demo fiddle here.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search