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.
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? 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.
The exception thrown when an error occurs during JSON serialization or deserialization.
NET 7, System. Text. Json supports polymorphic type hierarchy serialization and deserialization with attribute annotations.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With