using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Collections.ObjectModel;
using System.Collections.Concurrent;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json.Nodes;
public class DictionaryValueConverterDecorator<TItemConverter> : JsonConverterFactory where TItemConverter : JsonConverter, new()
readonly TItemConverter itemConverter = new TItemConverter();
public override bool CanConvert(Type typeToConvert) =>
GetStringKeyedDictionaryValueType(typeToConvert) != null && GetConcreteTypeToConvert(typeToConvert) != null;
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
if (!(GetStringKeyedDictionaryValueType(typeToConvert) is {} valueType)
|| !(GetConcreteTypeToConvert(typeToConvert) is {} concreteTypeToConvert))
throw new ArgumentException($"Invalid type {typeToConvert}");
var modifiedOptions = new JsonSerializerOptions(options);
modifiedOptions.Converters.Insert(0, itemConverter);
var actualInnerConverter = modifiedOptions.GetConverter(valueType);
return (JsonConverter)Activator.CreateInstance(typeof(DictionaryValueConverterDecoratorInner<,>).MakeGenericType(new [] { concreteTypeToConvert, valueType }), new object [] { actualInnerConverter })!;
static Type? GetStringKeyedDictionaryValueType(Type typeToConvert)
if (!(typeToConvert.GetDictionaryKeyValueType() is {} types))
if (types[0] != typeof(string))
static Type? GetConcreteTypeToConvert(Type typeToConvert) =>
typeToConvert.IsInterface switch
false when typeToConvert.GetConstructor(Type.EmptyTypes) != null =>
true when typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() is var generic && (generic == typeof(IDictionary<,>) || generic == typeof(IReadOnlyDictionary<,>)) =>
typeof(Dictionary<,>).MakeGenericType(typeToConvert.GetGenericArguments()),
internal class DictionaryValueConverterDecoratorInner<TDictionary, TValue> : JsonConverter<TDictionary> where TDictionary : IDictionary<string, TValue>, new()
readonly JsonConverter<TValue> innerConverter;
public DictionaryValueConverterDecoratorInner(JsonConverter<TValue> innerConverter) => this.innerConverter = innerConverter ?? throw new ArgumentNullException(nameof(innerConverter));
public override TDictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
if (reader.TokenType == JsonTokenType.Null)
return (TDictionary?)(object?)null;
else if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();
var dictionary = new TDictionary();
if (reader.TokenType == JsonTokenType.EndObject)
else if (reader.TokenType != JsonTokenType.PropertyName)
throw new JsonException();
var key = reader.GetString().ThrowOnNull();
var value = innerConverter.Read(ref reader, typeof(TValue), options);
dictionary.Add(key, value!);
public override void Write(Utf8JsonWriter writer, TDictionary value, JsonSerializerOptions options)
writer.WriteStartObject();
foreach (var pair in value)
writer.WritePropertyName(pair.Key);
if (value == null && !innerConverter.HandleNull)
innerConverter.Write(writer, pair.Value, options);
public static partial class JsonExtensions
public static ref Utf8JsonReader ReadAndAssert(ref this Utf8JsonReader reader) { if (!reader.Read()) { throw new JsonException(); } return ref reader; }
public static T ThrowOnNull<T>(this T? value, [System.Runtime.CompilerServices.CallerArgumentExpression(nameof(value))] string? paramName = null) where T : class => value ?? throw new ArgumentNullException(paramName);
public static class TypeExtensions
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();
public static class LinqExtensions
public static TSource? SingleOrDefaultIfMultiple<TSource>(this IEnumerable<TSource> source)
var elements = source.Take(2).ToArray();
return (elements.Length == 1) ? elements[0] : default(TSource);
public partial class Model
[JsonPropertyName("substances"),
JsonConverter(typeof(DictionaryValueConverterDecorator<SaturationJsonConverter>))]
protected ConcurrentDictionary<string, Saturation> Substances { get; private set; } = new();
public partial class Model
public IDictionary<string, Saturation> SubstancesSurrogate => Substances;
public string? Value { get; set; }
public class SaturationJsonConverter : JsonConverter<Saturation>
public override Saturation? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
new Saturation { Value = reader.GetString() };
public override void Write(Utf8JsonWriter writer, Saturation? value, JsonSerializerOptions options) =>
writer.WriteStringValue(value?.Value);
public static void Test()
["Item1"] = new Saturation { Value = "Item1" },
["Item2"] = new Saturation { Value = "Item2" },
var json = JsonSerializer.Serialize(model);
Assert.AreEqual("""{"substances":{"Item2":"Item2","Item1":"Item1"}}""", json);
var model2 = JsonSerializer.Deserialize<Model>(json);
var json2 = JsonSerializer.Serialize(model2);
Assert.That(JsonNode.DeepEquals(JsonNode.Parse(json), JsonNode.Parse(json2)));
Assert.That(model.SubstancesSurrogate.Count == model2?.SubstancesSurrogate.Count);
public static void Main()
Console.WriteLine("Environment version: {0} ({1}), {2}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version, Environment.OSVersion);
Console.WriteLine("System.Text.Json version: " + typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine("Failed with unhandled exception: ");