Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to deserialize a read only List with Json.Net

I have a class with an internal list. I don't want the user of this class to be able to interact with the list directly, since I want to both keep it sorted and to run a calculation (which depends on the order) before returning it.

I expose

AddItem(Item x) 

and a

IEnumerable<Item> Items
{
     get { // returns a projection of internal list }
}

Serialization worked OK, but deserializing left the list empty. I figured it was because I didn't have a setter. So I added one that allowed you to set the list, but only if the internal list was empty. But this didn't solve the problem, turns out NewtonSoft does not call the setter, it only calls the getter to get the list, and then adds each item to it, which, since my getter returns a projected list, those items get added to an object that is immediately disposed once deserialization is done.

How do I maintain a read-only access to my list, while at the same time allowing for somewhat straightforward deserialization?

like image 918
Neil N Avatar asked Dec 09 '14 19:12

Neil N


3 Answers

What worked for me was the following:

[JsonProperty(PropertyName = "TargetName")]
private List<SomeClass> _SomeClassList { get; set; }
public IReadOnlyList<SomeClass> SomeClassList
{
    get
    {
        return this._SomeClassList.AsReadOnly();
    }
}

Then, make a function to prevent SomeClassList to be serialized:

public bool ShouldSerializeSomeClassList() { return false; }

See https://www.newtonsoft.com/json/help/html/ConditionalProperties.htm (thanks Peska)

like image 181
Wotuu Avatar answered Nov 20 '22 12:11

Wotuu


Looks like there's a number of ways to do it, but one thing I did not want to do was to have to modify all of my data objects to be aware of how they should be serialized/deserialized.

One way to do this was to take some examples of DefaultContractResolver's others had done (but still didn't do what I needed to do) and modify them to populate readonly fields.

Here's my class that I'd like to Serialize/Deserialize

public class CannotDeserializeThis
{
    private readonly IList<User> _users = new List<User>();
    public virtual IEnumerable<User> Users => _users.ToList().AsReadOnly();

    public void AddUser(User user)
    {
        _users.Add(user);
    }
}

I could serialize this to: {"Users":[{"Name":"First Guy"},{"Name":"Second Guy"},{"Name":"Third Guy"}]}

But Deserializing this would leave the Users IEnumerable empty. The only way, I could find, around this was to either remove the '.ToList.AsReadonly' on the Users property or implement a DefaultContractResolver as such:

public class ReadonlyJsonDefaultContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var prop = base.CreateProperty(member, memberSerialization);
        if (!prop.Writable)
        {
            var property = member as PropertyInfo;
            if (property != null)
            {
                var hasPrivateSetter = property.GetSetMethod(true) != null;
                prop.Writable = hasPrivateSetter;

                if (!prop.Writable)
                {
                    var privateField = member.DeclaringType.GetRuntimeFields().FirstOrDefault(x => x.Name.Equals("_" + Char.ToLowerInvariant(prop.PropertyName[0]) + prop.PropertyName.Substring(1)));

                    if (privateField != null)
                    {
                        var originalPropertyName = prop.PropertyName;
                        prop = base.CreateProperty(privateField, memberSerialization);
                        prop.Writable = true;
                        prop.PropertyName = originalPropertyName;
                        prop.UnderlyingName = originalPropertyName;
                        prop.Readable = true;
                    }
                }
            }
        }

        return prop;
    }
}

The DefaultContractResolver is finding the corresponding private backing field, creating a property out of that, and renaming it to the public readonly property.

This assumes a convention, though. That your backing field starts with an underscore and is a lowercase version of your public property. For most of the code we were working with, this was a safe assumption. (e.g. 'Users' -> '_users', or 'AnotherPropertyName' -> '_anotherPropertyName')

like image 30
Jay Dubya Avatar answered Nov 20 '22 12:11

Jay Dubya


With Newtonsoft you can use a CustomCreationConverter<T> or the abstract JsonConverter, you have to implement the Create method and ReadJson.

The ReadJson method is where the converter will do the default deserialization calling the base method, from there, each item inside the readonly collection can be deserialized and added with the AddItem method.

Any custom logic can be implemented inside AddItem.

The last step is configuring this new converter for deserialization with an attribute [JsonConverter(typeof(NavigationTreeJsonConverter))] or within the JsonSerializerSettings

public class ItemsHolderJsonConverter : CustomCreationConverter<ItemsHolder>
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(ItemsHolder).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader,
                                   Type objectType,
                                   object existingValue,
                                   JsonSerializer serializer)
    {

        JObject jObject = JObject.Load(reader);

        ItemsHolder holder = base.ReadJson(CreateReaderFromToken(reader,jObject), objectType, existingValue, serializer) as ItemsHolder;

        var jItems = jObject[nameof(ItemsHolder.Items)] as JArray ?? new JArray();
        foreach (var jItem in jItems)
        {
            var childReader = CreateReaderFromToken(reader, jItem);
            var item = serializer.Deserialize<Item>(childReader);
            holder.AddItem(item);
        }

        return holder;
    }

    public override ItemsHolder Create(Type objectType)
    {
        return new ItemsHolder();
    }

    public static JsonReader CreateReaderFromToken(JsonReader reader, JToken token)
    {
        JsonReader jObjectReader = token.CreateReader();
        jObjectReader.Culture = reader.Culture;
        jObjectReader.DateFormatString = reader.DateFormatString;
        jObjectReader.DateParseHandling = reader.DateParseHandling;
        jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
        jObjectReader.FloatParseHandling = reader.FloatParseHandling;
        jObjectReader.MaxDepth = reader.MaxDepth;
        jObjectReader.SupportMultipleContent = reader.SupportMultipleContent;
        return jObjectReader;
    }
}
like image 1
animalito maquina Avatar answered Nov 20 '22 13:11

animalito maquina