6

I have an object which has several collection properties defined with public getter but private setter, In this case JsonConvert.PopulateObject adds the deserialized items to these collections leaving the existing items untouched.

I need a behavior when such member collections get cleared before deserialization.

I tried to manually clear the collections in a method marked with the [OnDeserializing] attribute.

The problem with that approach is that it will still clear the collections even if the collection property does not exist in the JSON string.

I need a way when only those collections get cleared which are actually defined in the JSON string. Those which are undefined should be kept untouched.

Thanks

Nir Schwartz
  • 1,723
  • 2
  • 15
  • 27
Zoltán Tamási
  • 12,249
  • 8
  • 65
  • 93
  • Have you tried setting `ObjectCreationHandling` to `Replace`? See [Serialization Settings](http://www.newtonsoft.com/json/help/html/SerializationSettings.htm) in the documentation. – Brian Rogers Feb 18 '16 at 15:57
  • 2
    No because as I understand that, it means that it replaces **the collection**, not the items. It would create a new list instance, which is not what I want (that's why the setter is private, no one should mess with the list instance due to databinding, etc.). – Zoltán Tamási Feb 18 '16 at 16:03

1 Answers1

9

Okay, so after some trip to Json.NET sources I found the following solution by inheriting a custom contract resolver from DefaultContractResolver.

I needed to override the array contract creation to add a deserialization callback. At this point the callback receives the concrete collection instance, so we can manipulate it (in this case clear it).

As long as I can determine, it is safe to use, but feel free to warn about any drawbacks of this method.

Note: Am I the only one who feels that this should probably be the default behavior?

public class CollectionClearingContractResolver : DefaultContractResolver
{
    protected override JsonArrayContract CreateArrayContract(Type objectType)
    {
        var c = base.CreateArrayContract(objectType);
        c.OnDeserializingCallbacks.Add((obj, streamingContext) =>
        {
            var list = obj as IList;
            if (list != null && !list.IsReadOnly)
                list.Clear();
        });
        return c;
    }
}

...

public class Test {
    public List<int> List { get; private set; }
    public Test() {
        List = new List<int>();
    }
}  

...

var myObj = new Test();
myObj.List.AddRange(new[] {1,2,3});
var listReference = myObj.List;    

JsonConvert.PopulateObject("{ List: [4, 5, 6] }", myObj, 
    new JsonSerializerSettings {
        ContractResolver = new CollectionClearingContractResolver(),
    });

myObj.List.ShouldEqual(listReference); // didn't recreate list
myObj.List.Count.ShouldEqual(3);
myObj.List.SequenceEqual(new[] { 4, 5, 6}).ShouldBeTrue();
Zoltán Tamási
  • 12,249
  • 8
  • 65
  • 93
  • 1
    I would add a check to see whether `obj` is an array before clearing it, since arrays are read-only. Or check [`IList.IsReadOnly`](https://msdn.microsoft.com/en-us/library/system.collections.ilist.isreadonly%28v=vs.110%29.aspx) for that matter. – dbc Feb 18 '16 at 17:40
  • I'm with you in that I think this should be the default behavior. – Russell Horwood Feb 08 '22 at 17:17