Alejandro Nagy Alejandro Nagy - 3 years ago 146
C# Question

Json.Net Class hierarchy with ObservableCollection and INotifyPropertyChange gets serialized but not deserialized

I find myself a bit lost on this one. I honestly can't see the error if it's just a class structure doesn't match JSON error. But I doubt it since it's the very same class structure I'm using to create the JSON.

If anyone can point me in the right direction, I'd be most greateful.

I've created a dotnetfiddle to avoid clutching the question with huge pieces of code. Here's the link: Fiddle

I generate that JSON with a console application that gets info on the DB schema. I use a common project with all the entities defined in it to load the data in memory and then generate the JSON from that structure. Then I use the same project with the same entities on another application to compare another DB schema to the JSON log. That application is unable to deserialize the JSON. Tried to provide a minimal example with a single class and as you can see on the fiddle...that doesn't deserialize either.
It is my understanding that ObservableCollections should in fact serialize and deserialize without issues, and that INotifyPropertyChange should not cause issues (as long as you're not trying to fire an event with a null reference). So...anyone has any idea what's going on here?.

EDIT: Forgot to mention. Notice how only the base type string gets deserialized...so it IS running some deserialization, just not of classes like ObservableCollection or user classes. Maybe that helps somehow to pinpoint the issue.

EDIT2: Added a trace writer and the JSON.Net trace is detecting the right type for the objects, so I'm guessing the issue is on converting types or initializing some of the types

Answer Source

The problem is in how your property getters work combined with the default ObjectCreationHandling setting in Json.Net. Allow me to explain:

By default, if a reference property has an existing (non-null) value during deserialization, Json.Net tries to reuse the existing instance and populate it instead of creating a new instance. To find out whether the property has a value, Json.Net calls the getter. In your case, the getter returns a new instance when the backing field is null, but, critically, it does not set the backing field to the new instance:

    get { return _header ?? new StoredProcedureDataHeader(); }

Json.Net then populates the new instance. Because the backing field was never set to the new instance, that instance ultimately gets thrown away. Json.Net never calls your setter because it assumes that your object already has a reference to the new instance, since it got that instance from the getter. Then, when you next call that getter after deserialization, you get a new, empty instance back instead of what you were expecting.

There are two ways to fix the problem:

  1. Change your getters to set the backing field whenever a new instance is created, for example:

    get 
    {
        if (_header == null)
        {
            _header = new StoredProcedureDataHeader();
        }
        return _header;
    }
    

OR

  1. Change the ObjectCreationHandling setting to Replace to force Json.Net to always create new instances when deserializing. Json.Net will then call the setter and not the getter, which I think is what you want.

    var settings = new JsonSerializerSettings
    {
        ObjectCreationHandling = ObjectCreationHandling.Replace
    };
    
    var data = JsonConvert.DeserializeObject<StoredProcedureData>(json, settings);
    

In your case, I would actually recommend that you apply both fixes. If you don't fix your getters (option 1), you could run into a similar issue elsewhere in your code. For example, you might have something like this:

var data = new StoredProcedureData();
data.Header.SPName = "foo";
if (data.Header.SPName == "foo")
{
    Console.WriteLine("foo");
}
else
{
    Console.WriteLine("oops");
}

Guess which value will be printed?

And option 2 will protect against possibly unexpected results if you happen to have initialized a collection somewhere to have a default set of values. For example, if you had something like this:

public StoredProcedureData()
{
    _funcRef = new ObservableCollection<string>();
    _funcRef.Add("Initialize");
}

then when you deserialize, you will get the default values plus the values from the JSON, which is probably not what you want. Setting ObjectCreationHandling to Replace will ensure that you will end up with just the values which were deserialized from the JSON.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download