Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can System.Text.Json.JsonSerializer serialize collections on a read-only property?

I'm having trouble getting the new System.Text.Json to deserialize collections stored on read-only properties.

Consider these classes:

public class SomeItem {
    public string Label { get; set; }
}

public class SomeObjectWithItems {

    public string Label { get; set; }

    // Note this property is read-only but the collection it points to is read/write
    public ObservableCollection<SomeItem> Items { get; }
        = new ObservableCollection<SomeItem>();
}

Here's the JSON:

{
  "Label": "First Set",
  "Items": [
    {
      "Label": "Item 1"
    },
    {
      "Label": "Item 2"
    },
    {
      "Label": "Item 3"
    },
    {
      "Label": "Item 4"
    }
  ]
}

Here's the code I'm running...

var json = ...;
var obj = JsonSerializer.deserialize<SomeObjectWithItems>(json);
Debug.WriteLine($"Item Count for '{obj.label}': {obj.Items.Count}");  

The above outputs the following:

Item Count for 'First Set': 0

If I change Items to be read/write, then it works, but so many of our models have read-only properties that hold mutable collections so I'm wondering if we can even use this.

Note: Json.NET handles this correctly, internally calling the 'Add' method on the existing collection rather than creating a new one, but I don't know how to achieve that outside of writing custom converters for all the classes we have defined.

like image 435
Mark A. Donohoe Avatar asked Jan 15 '20 09:01

Mark A. Donohoe


People also ask

What can JSON serialize?

JSON is a format that encodes objects in a string. Serialization means to convert an object into that string, and deserialization is its inverse operation (convert string -> object). If you serialize this result it will generate a text with the structure and the record returned.

Can JSON serialize a list?

Can JSON serialize a list? Json.NET has excellent support for serializing and deserializing collections of objects. To serialize a collection - a generic list, array, dictionary, or your own custom collection - simply call the serializer with the object you want to get JSON for.

What is a JSON serialization exception?

The exception thrown when an error occurs during JSON serialization or deserialization.

Is polymorphic deserialization possible in System text JSON?

NET 7, System. Text. Json supports polymorphic type hierarchy serialization and deserialization with attribute annotations.


1 Answers

This is by design for collections that don't have a setter. To avoid issues with adding to pre-populated collections (that the serializer doesn't instantiate) the deserializer uses "replace" semantics which requires the collection to have a setter.

Source: https://github.com/dotnet/corefx/issues/41433

There is currently an open issue for Support adding to collections if no setter

https://github.com/dotnet/corefx/issues/39477

My recommendation is continue to use Json.NET in this case unless you want to write a custom converter.

https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-5-0

Custom converter from GitHub, not tested this myself:

class MagicConverter : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert) =>
        !typeToConvert.IsAbstract &&
        typeToConvert.GetConstructor(Type.EmptyTypes) != null &&
        typeToConvert
            .GetProperties()
            .Where(x => !x.CanWrite)
            .Where(x => x.PropertyType.IsGenericType)
            .Select(x => new
            {
                Property = x,
                CollectionInterface = x.PropertyType.GetGenericInterfaces(typeof(ICollection<>)).FirstOrDefault()
            })
            .Where(x => x.CollectionInterface != null)
            .Any();

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => (JsonConverter)Activator.CreateInstance(typeof(SuperMagicConverter<>).MakeGenericType(typeToConvert))!;

    class SuperMagicConverter<T> : JsonConverter<T> where T : new()
    {
        readonly Dictionary<string, (Type PropertyType, Action<T, object>? Setter, Action<T, object>? Adder)> PropertyHandlers;
        public SuperMagicConverter()
        {
            PropertyHandlers = typeof(T)
                .GetProperties()
                .Select(x => new
                {
                    Property = x,
                    CollectionInterface = !x.CanWrite && x.PropertyType.IsGenericType ? x.PropertyType.GetGenericInterfaces(typeof(ICollection<>)).FirstOrDefault() : null
                })
                .Select(x =>
                {
                    var tParam = Expression.Parameter(typeof(T));
                    var objParam = Expression.Parameter(typeof(object));
                    Action<T, object>? setter = null;
                    Action<T, object>? adder = null;
                    Type? propertyType = null;
                    if (x.Property.CanWrite)
                    {
                        propertyType = x.Property.PropertyType;
                        setter = Expression.Lambda<Action<T, object>>(
                            Expression.Assign(
                                Expression.Property(tParam, x.Property),
                                Expression.Convert(objParam, propertyType)),
                            tParam,
                            objParam)
                            .Compile();
                    }
                    else
                    {
                        if (x.CollectionInterface != null)
                        {
                            propertyType = x.CollectionInterface.GetGenericArguments()[0];
                            adder = Expression.Lambda<Action<T, object>>(
                                Expression.Call(
                                    Expression.Property(tParam, x.Property),
                                    x.CollectionInterface.GetMethod("Add"),
                                    Expression.Convert(objParam, propertyType)),
                                tParam,
                                objParam)
                                .Compile();
                        }
                    }
                    return new
                    {
                        x.Property.Name,
                        setter,
                        adder,
                        propertyType
                    };
                })
                .Where(x => x.propertyType != null)
                .ToDictionary(x => x.Name, x => (x.propertyType!, x.setter, x.adder));
        }
        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => throw new NotImplementedException();
        public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var item = new T();
            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject)
                {
                    break;
                }
                if (reader.TokenType == JsonTokenType.PropertyName)
                {
                    if (PropertyHandlers.TryGetValue(reader.GetString(), out var handler))
                    {
                        if (!reader.Read())
                        {
                            throw new JsonException($"Bad JSON");
                        }
                        if (handler.Setter != null)
                        {
                            handler.Setter(item, JsonSerializer.Deserialize(ref reader, handler.PropertyType, options));
                        }
                        else
                        {
                            if (reader.TokenType == JsonTokenType.StartArray)
                            {
                                while (true)
                                {
                                    if (!reader.Read())
                                    {
                                        throw new JsonException($"Bad JSON");
                                    }
                                    if (reader.TokenType == JsonTokenType.EndArray)
                                    {
                                        break;
                                    }
                                    handler.Adder!(item, JsonSerializer.Deserialize(ref reader, handler.PropertyType, options));
                                }
                            }
                            else
                            {
                                reader.Skip();
                            }
                        }
                    }
                    else
                    {
                        reader.Skip();
                    }
                }
            }
            return item;
        }
    }
}

Usage:

var options = new JsonSerializerOptions { Converters = { new MagicConverter() } };

var adsfsdf = System.Text.Json.JsonSerializer.Deserialize<Grrrr>("{\"Meow\":[1,2,3]}", options);
var adsfsdf2 = System.Text.Json.JsonSerializer.Deserialize<Grrrr>("{\"Meow\":null}", options);
var adsfsdf3 = System.Text.Json.JsonSerializer.Deserialize<Grrrr>("{\"Meow\":[1,2,3],\"Rawr\":\"asdf\"}", options);
var adsfsdf4 = System.Text.Json.JsonSerializer.Deserialize<Grrrr>("{\"Meow\":[1,2,3],\"Rawr\":null}", options);
var adsfsdf5 = System.Text.Json.JsonSerializer.Deserialize<Grrrr>("{\"Meow\":[1,2,3],\"Rawr\":\"asdf\",\"SubGrr\":{\"Meow\":[1,2,3],\"Rawr\":\"asdf\"}}", options);

Source:

https://github.com/dotnet/runtime/issues/30258#issuecomment-564847072

like image 152
Ogglas Avatar answered Oct 21 '22 13:10

Ogglas