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
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
Your problem is here:
You check to see whether
jsonTypeInfo.Type
is assignable toIMessageContent
, and then add derived typesFoo
andBar
. ButIMessageContent
has many derived types, includingFoo
itself. Then when your code is invoked, it is actually attempting to assign the derived typeBar
toFoo
, which fails, becauseBar
is not a derived type ofFoo
.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:You would then use the modifier as follows:
Notes:
DefaultJsonTypeInfoResolver
because modifiers can more easily be combined and reused.Demo fiddle here.