using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json.Nodes;
public string Label { get; set; }
public class SomeObjectWithItems {
public string Label { get; set; }
public ObservableCollection<SomeItem> Items { get; }
= new ObservableCollection<SomeItem>();
public class SomeObjectWithItems<TCollection> : SomeObjectWithItems<TCollection, TCollection> where TCollection : ICollection<SomeItem>, new()
public class SomeObjectWithItems<TCollection, TConcreteCollection>
where TCollection : IEnumerable<SomeItem>
where TConcreteCollection : TCollection, new()
public string Label { get; set; }
public TCollection Items { get; }
= new TConcreteCollection();
public static partial class JsonExtensions
public static Action<JsonTypeInfo> CreateReadOnlyCollectionPropertySetters(Type type) => typeInfo =>
if (!type.IsAssignableFrom(typeInfo.Type))
CreateReadOnlyCollectionPropertySetters(typeInfo);
public static void CreateReadOnlyCollectionPropertySetters(JsonTypeInfo typeInfo)
if (typeInfo.Kind != JsonTypeInfoKind.Object)
foreach (var property in typeInfo.Properties)
if (property.Get != null && property.Set == null && property.PropertyType.GetCollectionItemType() is {} itemType)
var method = typeof(JsonExtensions).GetMethod(nameof(JsonExtensions.CreateGetOnlyCollectionPropertySetter),
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!;
var genericMethod = method.MakeGenericMethod(new[] { itemType });
var setter = genericMethod.Invoke(null, new object[] { property }) as Action<object, object?>;
static Action<Object,Object?>? CreateGetOnlyCollectionPropertySetter<TItem>(JsonPropertyInfo property)
if (property.Get == null)
(var getter, var name) = (property.Get, property.Name);
var oldValue = (ICollection<TItem>?)getter(obj);
var newValue = value as ICollection<TItem>;
if (newValue == oldValue)
else if (oldValue == null || oldValue.IsReadOnly)
throw new JsonException("Cannot populate list ${name} in ${obj}.");
foreach (var item in newValue)
public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
static readonly Dictionary<Type, Type> InterfaceToOpenConcreteTypes = new ()
[typeof(IEnumerable<>)] = typeof(List<>),
[typeof(ICollection<>)] = typeof(List<>),
[typeof(IList<>)] = typeof(List<>),
[typeof(ISet<>)] = typeof(HashSet<>),
static readonly HashSet<Type> MutableCollectionBaseTypes = new []
static Type? GetCollectionItemType(this Type type)
if (type.IsArray || type.IsPrimitive)
else if (type.IsInterface)
return type.IsGenericType && InterfaceToOpenConcreteTypes.TryGetValue(type.GetGenericTypeDefinition(), out var _)
? type.GetGenericArguments()[0] : null;
return type.BaseTypesAndSelf()
.Where(t => t.IsGenericType && MutableCollectionBaseTypes.Contains(t.GetGenericTypeDefinition()))
.FirstOrDefault()?.GetGenericArguments()[0];
public static void Test()
TestGeneric<List<SomeItem>>(json);
TestGeneric<Collection<SomeItem>>(json);
TestGeneric<HashSet<SomeItem>>(json);
TestGeneric<IList<SomeItem>, List<SomeItem>>(json);
TestGeneric<ICollection<SomeItem>, List<SomeItem>>(json);
TestGeneric<IEnumerable<SomeItem>, List<SomeItem>>(json);
TestGeneric<ISet<SomeItem>, HashSet<SomeItem>>(json);
static void Test(string json)
var options = new JsonSerializerOptions
TypeInfoResolver = new DefaultJsonTypeInfoResolver
JsonExtensions.CreateReadOnlyCollectionPropertySetters(typeof(SomeObjectWithItems)),
var obj = JsonSerializer.Deserialize<SomeObjectWithItems>(json, options);
Console.WriteLine($"Item Count for '{obj?.Label}': {obj?.Items.Count}");
Console.WriteLine("Re-serialized {0};", obj);
var newJson = JsonSerializer.Serialize(obj, options);
Console.WriteLine(newJson);
Assert.AreEqual(4, obj?.Items.Count ?? -1);
static void TestGeneric<TCollection, TConcreteCollection>(string json)
where TCollection : IEnumerable<SomeItem>
where TConcreteCollection : TCollection, new()
var options = new JsonSerializerOptions
TypeInfoResolver = new DefaultJsonTypeInfoResolver
JsonExtensions.CreateReadOnlyCollectionPropertySetters(typeof(SomeObjectWithItems<TCollection, TConcreteCollection>)),
var obj = JsonSerializer.Deserialize<SomeObjectWithItems<TCollection, TConcreteCollection>>(json, options);
Console.WriteLine($"Item Count for '{obj?.Label}': {obj?.Items.Count()}");
Console.WriteLine("Re-serialized {0};", obj);
var newJson = JsonSerializer.Serialize(obj, options);
Console.WriteLine(newJson);
Assert.AreEqual(4, obj?.Items.Count() ?? -1);
static void TestGeneric<TCollection>(string json) where TCollection : ICollection<SomeItem>, new ()
TestGeneric<TCollection, TCollection>(json);
public static void Main(string[] args)
Console.WriteLine("Environment version: {0} ({1})", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version);
Console.WriteLine("{0} version: {1}", typeof(JsonSerializer).Assembly.GetName().Name, typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine("Failed with unhandled exception: ");