I have currently been infront of the same problem in Blazor app, so I was not been able to switch to Newtonsoft.Json
easily. I found two ways. One is in reality hack. You can create custom converter, where you use Newtonsoft.Json
in Read/Write methods, instead of System.Text.Json
. But that was not what I was looking for. So I make some custom interface converter. I have some working solution, that not have been tested widely, but it's working for what I need.
Situation
I have a List<TInterface>
with objects implementing TInterface
. But there is a lot of different implementations. I need to serialize data on server, and deserialize on client WASM app, with all the data. For JavaScript deserialization, the implementation with custom Write method mentioned later is enough. For deserialization in C#, I need to know the exact types of objects serialized for each item in the list.
First, I need JsonConverterAttribute
on interface. So I was following this article: https://khalidabuhakmeh.com/serialize-interface-instances-system-text-json. There is some implementation of Writer
, that will handle interface type. But there is not Read
implementation. So I had to make my own.
How
- modify
Write
method to write type of object as first property to JSON object. Using JsonDocument to get all properties from original object.
- when reading the JSON, use clonned reader (as suggested in Microsoft docs for custom json converters) to find first property named
$type
with type information. Than create instance of that type and use type to deserialize data from original reader.
Code
Interface and classes:
[JsonInterfaceConverter(typeof(InterfaceConverter<ITest>))]
public interface ITest
{
int Id { get; set; }
string Name { get; set; }
}
public class ImageTest : ITest
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Image { get; set; } = string.Empty;
}
public class TextTest : ITest
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
public bool IsEnabled { get; set; }
}
Interface converter attribute:
// Source: https://khalidabuhakmeh.com/serialize-interface-instances-system-text-json
[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)]
public class JsonInterfaceConverterAttribute : JsonConverterAttribute
{
public JsonInterfaceConverterAttribute(Type converterType)
: base(converterType)
{
}
}
Converter:
public class InterfaceConverter<T> : JsonConverter<T>
where T : class
{
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Utf8JsonReader readerClone = reader;
if (readerClone.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
readerClone.Read();
if (readerClone.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
string propertyName = readerClone.GetString();
if (propertyName != "$type")
{
throw new JsonException();
}
readerClone.Read();
if (readerClone.TokenType != JsonTokenType.String)
{
throw new JsonException();
}
string typeValue = readerClone.GetString();
var instance = Activator.CreateInstance(Assembly.GetExecutingAssembly().FullName, typeValue).Unwrap();
var entityType = instance.GetType();
var deserialized = JsonSerializer.Deserialize(ref reader, entityType, options);
return (T)deserialized;
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
switch (value)
{
case null:
JsonSerializer.Serialize(writer, (T)null, options);
break;
default:
{
var type = value.GetType();
using var jsonDocument = JsonDocument.Parse(JsonSerializer.Serialize(value, type, options));
writer.WriteStartObject();
writer.WriteString("$type", type.FullName);
foreach (var element in jsonDocument.RootElement.EnumerateObject())
{
element.WriteTo(writer);
}
writer.WriteEndObject();
break;
}
}
}
}
Usage:
var list = new List<ITest>
{
new ImageTest { Id = 1, Name = "Image test", Image = "some.url.here" },
new TextTest { Id = 2, Name = "Text test", Text = "kasdglaskhdgl aksjdgl asd gasdg", IsEnabled = true },
new TextTest { Id = 3, Name = "Text test 2", Text = "asd gasdg", IsEnabled = false },
new ImageTest { Id = 4, Name = "Second image", Image = "diff.url.here" }
};
var json = JsonSerializer.Serialize(list);
var data = JsonSerializer.Deserialize<List<ITest>>(json);
// JSON data
// [
// {
// "$type":"ConsoleApp1.ImageTest",
// "Id":1,
// "Name":"Image test",
// "Image":"some.url.here"
// },
// {
// "$type":"ConsoleApp1.TextTest",
// "Id":2,
// "Name":"Text test",
// "Text":"kasdglaskhdgl aksjdgl asd gasdg",
// "IsEnabled":true
// },
// {
// "$type":"ConsoleApp1.TextTest",
// "Id":3,
// "Name":"Text test 2",
// "Text":"asd gasdg",
// "IsEnabled":false
// },
// {
// "$type":"ConsoleApp1.ImageTest",
// "Id":4,
// "Name":"Second image",
// "Image":"diff.url.here"
// }
// ]
Edit:
I made a NuGet package with this logic. You can download it here: InterfaceConverter.SystemTextJson
Edit 26.3.2022:
The NuGet package version has implemented more logic, eg. looking for type in all referenced assemblies.