At the time of writing this you should use a custom converter with System.Text.Json.
Here's a converter for ReadOnlyDictionary<TKey, TValue>
and derived types. It assumes that all read-only dictionaries have a constructor that accepts an IDictionary<TKey, TValue>
or similar.
It simply deserializes the JSON into a normal Dictionary<TKey, TValue>
and then constructs the ReadOnlyDictionary type with that dictionary as an argument.
using System.Collections.ObjectModel;
using System.Reflection;
namespace System.Text.Json.Serialization
{
public class JsonReadOnlyDictionaryConverter : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
if (!typeToConvert.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)))
return false;
if ((typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() != typeof(ReadOnlyDictionary<,>)) &&
!typeof(ReadOnlyDictionary<,>).IsSubclassOfRawGeneric(typeToConvert))
return false;
return true;
}
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var iReadOnlyDictionary = typeToConvert.GetInterfaces().First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>));
Type keyType = iReadOnlyDictionary.GetGenericArguments()[0];
Type valueType = iReadOnlyDictionary.GetGenericArguments()[1];
JsonConverter converter = (JsonConverter)Activator.CreateInstance(
typeof(ReadOnlyDictionaryConverterInner<,>).MakeGenericType(keyType, valueType),
BindingFlags.Instance | BindingFlags.Public,
binder: null, args: null, culture: null);
return converter;
}
private class ReadOnlyDictionaryConverterInner<TKey, TValue> : JsonConverter<IReadOnlyDictionary<TKey, TValue>>
where TKey : notnull
{
public override IReadOnlyDictionary<TKey, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var dictionary = JsonSerializer.Deserialize<Dictionary<TKey, TValue>>(ref reader, options: options);
if (dictionary == null)
return null;
return (IReadOnlyDictionary<TKey, TValue>)Activator.CreateInstance(
typeToConvert, BindingFlags.Instance | BindingFlags.Public,
binder: null, args: new object[] { dictionary }, culture: null);
}
public override void Write(Utf8JsonWriter writer, IReadOnlyDictionary<TKey, TValue> dictionary, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, dictionary, options);
}
}
}
You could turn it into a custom JsonConverterAttribute
and decorate your class/property with it (which I prefer):
namespace System.Text.Json.Serialization
{
public class JsonReadOnlyDictionaryAttribute : JsonConverterAttribute
{
public JsonReadOnlyDictionaryAttribute() : base(typeof(JsonReadOnlyDictionaryConverter))
{
}
}
}
Or use the JsonSerializerOptions
:
var serializeOptions = new JsonSerializerOptions
{
Converters =
{
new JsonReadOnlyDictionaryConverter()
}
};
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);