Polymorphic Serialization & Deserialziation of Interface implementations in dotnet
I will preface this by saying this is not a brilliant idea, as you lose the benefit of a descriptive model (especially in swagger), and its somewhat brittle if you add more types. Honestly it started as a “It would be nice if I could do this in this scenario..” and just went down the rabbit hole. As per most my posts here this is more my personal notes to refer to later which may help others also searching, so YMMV.
Its not uncommon to have code which uses abstraction or inheritence in any programming language, for example a Dog, Cat and Llama all inheriting from the IAnimal interface. It’s also not uncommon to use dependency injection to determine the concrete type you wish to use for a given interface. But what if we wanted to do something a little… stupid.
Taking the above example into consideration, what if we want a model to have a List/Collection of IAnimals which can accept any derivative type within the same Web API message request? (and not just multiple lists, but a single bound field which can accept any combination).
Why?! (and other things you can try first)
Why not?! There are a few really good resources on this topic already including:
- If you are using .net7 or preview versions of dotnet, you may be able to use the new built in JsonDerivedType attribute as suggested here
- Microsofs dotnet docs on writing a custom json converter, but this mostly focusses on manually specifying the object with the JsonReader and JsonWriter which means overhead for model changes.
- Some possibilities using libraries like JsonSubTypes https://github.com/manuc66/JsonSubTypes which didn’t work for me, but may for others.
- So many SO posts on this topic, mostly around people asking why their interfaces won’t serialise, but the information is varied to say the least and oddly so many examples failed to include an example for the Write, only a read (as in, you could not Serialize).. There was a really good solution by a Demetrius here which I got working OK for single objects but misbehaved a little for lists of mixed types.
I also didn’t want to use dynamics, JObjects, reflection and such, but all of those would also be an alternative.
Onto the code
This extends the in-built System.Text
.Json, rather than rely on a third party package and was all built in .net6
In the example below I am specifically writing a converter which will decorate a List<IAnimal>, which importantly has a field we will use to allow every implementation to be identified and used as a discriminator. In this case every animal must have a type – here I use an enumeration to keep the code clean. (Note: If using an enum, you will want to have JsonStringEnumConverter as a converter in your JSON Options)
public interface IAnimal {
EnAnimalType Type { get; }
}
In the Read override, we loop over all the objects we have been provided, and use the type discriminator to deserialise based on the parsed value.
In the Write override, we do the same again, iterating over the items and casting them appropriately before a serialize.
public class MyAnimalTypeJsonTypeConverter : JsonConverter<List<IAnimal>>
{
public override List<IAnimal>
Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
return default;
List<IAnimal> items = default;
foreach (JsonObject jsonObject in JsonSerializer.Deserialize<List<JsonObject>>(ref reader, options)!)
{
IAnimal item = jsonObject["Type"]?.GetValue<string>() switch
{
"Dog" => jsonObject.Deserialize<Dog>(options)!,
"Cat" => jsonObject.Deserialize<Cat>(options)!,
_ => null
};
if (item is null) continue;
items ??= new();
items.Add(item);
}
return items;
}
public override void Write(Utf8JsonWriter writer, List<IAnimal> value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var item in value)
{
if (item.Type == EnAnimalType.Dog)
{
JsonSerializer.Serialize<Dog>(writer,item as Dog);
}
if (item.Type == EnAnimalType.Cat)
{
JsonSerializer.Serialize<Cat>(writer, item as Cat);
}
}
writer.WriteEndArray();
}
}
Then simply adding the attribute to my field
[JsonConverter(typeof(MyAnimalTypeJsonTypeConverter))]
public List<IAnimal>? Animals{ get; set; }
Enhancements I couldn’t get working (Disallowed options, extensions…)
This code is very specific to a certain interface and I don’t like lots of similar classes, so I attempted to use C# generics to take in a TMyObject
as the type (Easy) the same way the SO post linked above does, as well as enhancing the attribute to take in some form of array/params that would be the Enum/Type mapping. Sadly params on these JsonConverters aren’t really supported however @eiriktsarpalis on Github had a nice (and working) solution to getting around the fact System.Text.Json would not accept parameters, but sadly still ended up with the issue that the params always have to be constants for attributes so I couldn’t find a “Nice” solution for decorating the field. If anybody finds a nice workaround do comment!
Closing thoughts..
As stated, this is more of a stream of consciousness for me to refer back to at a later date, but if saves you a load of frustrated googling with the right keywords then hurrah.