Following How to use default serialization in a custom System.Text.Json JsonConverter?, you are trying to generate a default serialization by copying the current JsonSerializerOptions
and removing the current converter from its converters list. Your problem seems to be here:
newOptions.Converters.Remove(this);
You are using the factory converter pattern, and this line removes the inner, nested JsonDictionaryConverter<TDictionary, TKey, TValue>
from newOptions.Converters
. However, you need to remove the outer factory from the converters list, not the inner generated converter, as it is the factory that was added into the converters list previously.
The following is a working JsonConverterFactory
that deserializes any IDictionary<string, TValue>
that might be an array. When re-serializing, it removes itself from the converter list which results in default serialization:
public class JsonDictionaryConverterFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert) => TryGetDictionaryType(typeToConvert, out _, out _, out _);
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
TryGetDictionaryType(typeToConvert, out var dictionaryType, out var keyType, out var valueType);
var converterType = typeof(JsonDictionaryConverter<,,>).MakeGenericType(dictionaryType, keyType, valueType);
return (JsonConverter)Activator.CreateInstance(converterType, options, this);
}
static bool TryGetDictionaryType(Type typeToConvert, out Type dictionaryType, out Type keyType, out Type valueType)
{
var keyValueTypes = typeToConvert.GetDictionaryKeyValueType();
if (keyValueTypes != null && keyValueTypes.Length == 2)
{
// Todo; maybe put a restriction here on keyType, such as keyType == typeof(string).
if (typeToConvert.IsInterface)
{
var concreteType = typeof(Dictionary<,>).MakeGenericType(keyValueTypes);
if (typeToConvert.IsAssignableFrom(concreteType))
{
dictionaryType = concreteType;
keyType = keyValueTypes[0];
valueType = keyValueTypes[1];
return true;
}
}
else
{
if (typeToConvert.GetConstructor(Type.EmptyTypes) != null)
{
dictionaryType = typeToConvert;
keyType = keyValueTypes[0];
valueType = keyValueTypes[1];
return true;
}
}
}
dictionaryType = keyType = valueType = null;
return false;
}
class JsonDictionaryConverter<TDictionary, TKey, TValue> : JsonConverter<TDictionary> where TDictionary : IDictionary<TKey, TValue>, new()
{
readonly JsonSerializerOptions modifiedOptions;
public JsonDictionaryConverter(JsonSerializerOptions options, JsonDictionaryConverterFactory factory) => this.modifiedOptions = options.CopyAndRemoveConverter(factory.GetType());
public override void Write(Utf8JsonWriter writer, TDictionary value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, modifiedOptions);
public override TDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.StartObject:
return (TDictionary)JsonSerializer.Deserialize(ref reader, typeToConvert, modifiedOptions);
case JsonTokenType.StartArray:
var list = JsonSerializer.Deserialize<List<KeyValuePair<TKey, TValue>>>(ref reader, modifiedOptions);
var dictionary = new TDictionary();
foreach (var item in list)
dictionary.Add(item);
return dictionary;
default:
throw new JsonException();
}
}
}
}
public static class JsonExtensions
{
public static JsonSerializerOptions CopyAndRemoveConverter(this JsonSerializerOptions options, Type converterType)
{
var copy = new JsonSerializerOptions(options);
for (var i = copy.Converters.Count - 1; i >= 0; i--)
if (copy.Converters[i].GetType() == converterType)
copy.Converters.RemoveAt(i);
return copy;
}
public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
=> (type ?? throw new ArgumentNullException()).IsInterface ? new[] { type }.Concat(type.GetInterfaces()) : type.GetInterfaces();
public static IEnumerable<Type []> GetDictionaryKeyValueTypes(this Type type)
=> type.GetInterfacesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IDictionary<,>)).Select(t => t.GetGenericArguments());
public static Type [] GetDictionaryKeyValueType(this Type type)
=> type.GetDictionaryKeyValueTypes().SingleOrDefaultIfMultiple();
// Copied from this answer https://stackoverflow.com/a/25319572
// By https://stackoverflow.com/users/3542863/sean-rose
// To https://stackoverflow.com/questions/3185067/singleordefault-throws-an-exception-on-more-than-one-element
public static TSource SingleOrDefaultIfMultiple<TSource>(this IEnumerable<TSource> source)
{
var elements = source.Take(2).ToArray();
return (elements.Length == 1) ? elements[0] : default(TSource);
}
}
And then add to options as follows:
var options = new JsonSerializerOptions
{
Converters = { new JsonDictionaryConverterFactory() },
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
Notes:
I set PropertyNamingPolicy = JsonNamingPolicy.CamelCase
because your "key"
and "value"
property names are camel cased. If you do not want to use camel case globally, inside ReadJson()
you may wish to deserialize to a List<KeyValueDTO>
(such as the one shown in this answer to System.Text.Json Serialize dictionary as array) with explicitly camel-cased property names, rather than to a List<KeyValuePair<TKey, TValue>>
.
You may want to add some restrictions in TryGetDictionaryType()
on the key type, e.g. to only allow string
type keys.
Demo fiddle here.