1

I was trying out Json.net's ability to serialize and deserialize dictionaries, and thanks to this post I was able to find a good solution to serializing dictionaries in a simple way.

It was working great, but in a certain circumstance it broke in a (to me) nonsensical way such that I couldn't help but spend the next three hours debugging.

Here is the problem. When serializing this class

public class ReferenceTesting
{
    public List<Scenario> scenarios = new List<Scenario>();
    private Dictionary<Scenario, float> _Dict = new Dictionary<Scenario, float>();
    [JsonProperty]
    public List<KeyValuePair<Scenario, float>> SerializedDict
    {
        get { return _Dict.ToList(); }
        set { _Dict = value.ToDictionary(x => x.Key, x => x.Value); }
    }
    public ReferenceTesting(int number = 0)
    {
        for (int i = 0; i < number; i++)
        {
            Scenario s1 = new Scenario();
            scenarios.Add(s1);
            _Dict.Add(s1, i);
        }
    }
    public override string ToString()
    {
        string s = "";
        for (int i = 0; i < scenarios.Count(); i++)
        {
            Scenario scenario = scenarios[i];
            s += $"scenario{i} \n";
        }
        foreach (KeyValuePair<Scenario, float> scenario in SerializedDict)
        {
            s += $"Key: {scenario.Key}, Value: {scenario.Value} \n";
        }
        return s;
    }
}

Everything works as expected, meaning when I instantiate

new Reference(3);

and then serialize and deserialize, I end up with an object with as expected 3 items in the list, and 3 items in the dictionary.

Output:

scenario0 
scenario1 
scenario2 
Key: Scenario, Value: 0 
Key: Scenario, Value: 1 
Key: Scenario, Value: 2 

However, by adding the default constructor

public ReferenceTesting() { }

the serialization works, writing out 3 items in list and dictionary, but deserialization does not work with the property. Meaning I end up with

scenario0 
scenario1 
scenario2 

as output.

The big surprise with this is that the two constructors do the exact same thing - which is nothing when number = 0 (which it is when Json.net creates it, I doublechecked). So this means that the serializer has to be doing something under the hood to treat the property differently if there is or is not a default constructor.

TheForgot3n1
  • 212
  • 2
  • 11
  • I don't really have an idea but someone might want to check Json.NET source code [here](https://github.com/JamesNK/Newtonsoft.Json)? – Corentin Pane May 29 '20 at 06:40
  • It seems, that default [constructor handling](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_ConstructorHandling.htm) is looking for public default constructor first – Pavel Anikhouski May 29 '20 at 06:46
  • @PavelAnikhouski My thought is, regardless of whether it picks the default constructor or not, if both constructors have the exact same outcome, the result should ultimately be the same... – TheForgot3n1 May 29 '20 at 06:58
  • 1
    *"if both constructors have the exact same outcome"* - but they don't, the default (parameterless) constructor will not call another constructor automagically, you forgot to add `:this(0)`. Did you set breakpoint to test? I am not sure about precedence, but my guess optional parameter has higher, so it's called always, unless you use reflection to specifically use parameterless constructor (which json deserialization does). – Sinatr May 29 '20 at 07:27
  • 1
    No, default has [precedence](https://dotnetfiddle.net/NG0ATL). Anyway just use [this(0)](https://dotnetfiddle.net/SxmA2u). – Sinatr May 29 '20 at 07:34
  • @sinatr Yes, they do have the same outcome _in my code_. In my example, there is no difference between the two constructors when calling new ReferenceTesting(0) and new ReferenceTesting(). When number is 0, the for loops is immediately skipped, and the constructor just ends. So why is the output so different? – TheForgot3n1 May 29 '20 at 08:34
  • *"When number is 0, the for loops is immediately skipped"* - true, my bad. How do both jsons look like? Can you prepare [mcve]? You have mistake (should be `new ReferenceTesting(3)`?) and it's not clear how exactly you are getting both outputs you show. – Sinatr May 29 '20 at 10:00
  • 1
    What is the definition of `Scenario`? Please provide a [mre] so we can reproduce the problem. – Brian Rogers May 29 '20 at 21:56
  • Your problem isn't what you think it is. Your problem is that using a surrogate `List` property doesn't work at all for deserialization. But when you use the parameterized constructor, the constructor itself adds three items to the dictionary, which hides the error. Instead you need to use a surrogate **array**. For why see [Why are all the collections in my POCO are null when deserializing some valid json with the .NET Newtonsoft.Json component](https://stackoverflow.com/a/32493007/3744182), of which this might be a duplicate. – dbc May 30 '20 at 01:43
  • @dbc I believe the post you linked is the answer, but it isn't because the constructor adds three items - it doesn't do anything when number = 0. – TheForgot3n1 May 31 '20 at 08:52
  • OK the post I linked to explains why deserialization breaks when there is a default constructor. The reason it works with a non-default constructor is that, when a parameterized constructor exists, Json.NET completely reads the JSON object, deserializes all the properties to their known types, and only then constructs the object and sets the properties not passed into the constructor. The `List>` setter *does* get called when this algorithm is used. – dbc May 31 '20 at 15:29
  • By the way your current deserialization creates duplicates scenarios in the `scenarios` and `_Dict` collections, see https://dotnetfiddle.net/QoT0cf. One way to fix this would be to serialize *just* `_Dict` (assuming that all scenarios are in the dictionary). Another would be to use [`PreserveReferencesHandling`](https://www.newtonsoft.com/json/help/html/PreserveObjectReferences.htm), e.g. by adding `[JsonObject(IsReference = true)]` to `Scenario` as shown in https://dotnetfiddle.net/4AYTMV. – dbc May 31 '20 at 15:42
  • (`PreserveReferencesHandling` doesn't work for objects with parameterized constructors though, so make sure `Scenario` doesn't have one.) Actually, do my comments constitute an answer that is sufficiently different from [Why are all the collections in my POCO are null when deserializing some valid json with the .NET Newtonsoft.Json component](https://stackoverflow.com/a/32493007/3744182) to warrant writing an answer here? – dbc May 31 '20 at 15:42
  • Your explanation of why it works with a parameterized constructor is a really concise and good one. Maybe we could reformulate my question to be something more minimalistic, such as with your dotnetfiddle example, and then you could write an answer to that? This has helped me greatly by the way. – TheForgot3n1 May 31 '20 at 22:46

1 Answers1

2

How does Json.NET deserialize an object differently when it has a parameterized constructor vs. when it has a default constructor?

Json.NET is a streaming deserializer. Whenever possible it deserializes as it streams through the JSON rather than preloading the complete JSON into an intermediate representation before final deserialization.

Thus, when deserializing a JSON object with a default constructor, it first constructs the corresponding .Net object. It then recursively populates the object's .Net members by streaming through the key/value pairs in the JSON until the end of the JSON object. For each pair encountered, it finds the corresponding .Net member. If the value is a primitive type, it deserializes the primitive and sets the value. But if the value is a complex type (JSON object or array) it constructs the child object if necessary, sets the value back in the parent, and then populates it recursively as it continues to stream.

However, when deserializing an object with a parameterized constructor, Json.NET cannot use this streaming algorithm and instead must first fully deserialize the JSON object to an intermediate table of deserialized .Net name/value pairs, matching each JSON value to its corresponding .Net constructor argument or property by name and then deserializing to the type declared in .Net. Only then can the object be constructed by passing the deserialized constructor parameters into the constructor, and setting the remainder as property values.

For details on this process, see

(There is a third algorithm for ISerializable objects which does not apply in your case.)

Why is my surrogate public List<KeyValuePair<Scenario, float>> SerializedDict property not deserialized correctly when deserializing via a default constructor?

The reason is explained in this answer to Why are all the collections in my POCO are null when deserializing some valid json with the .NET Newtonsoft.Json component, and arises about of the specifics of Json.NET's Populate() algorithm:

  1. It calls the getter in the parent class to get the current value of the property being deserialized.

  2. If null, and unless a custom constructor is being used, it allocates an instance of the property's returned type (using the JsonContract.DefaultCreator method for the type).

  3. It calls the setter in the parent to set the allocated instance back into the parent.

  4. It proceeds to populate the instance of the type.

  5. It does not set the instance back a second time, after it has been populated.

Thus the setter for SerializedDict is not called after the list is populated.

But when the parent class has a parameterized constructor, the property value SerializedDict is fully deserialized before its parent is constructed, so the setter is called with a fully populated surrogate list.

How can I create a surrogate collection property that works in both scenarios?

You can use an array instead of a list. Since an array cannot be resized, it must be fully deserialized and populated before it can be set back in the parent object:

public class ReferenceTesting
{
    public KeyValuePair<Scenario, float> [] SerializedDict
    {
        get { return _Dict.ToArray(); }
        set { _Dict = value.ToDictionary(x => x.Key, x => x.Value);  }
    }
    // Remainder unchanged

You could make the array property be private if you want, by marking it with [JsonProperty].

By the way, your current deserialization creates duplicate Scenario objects in the scenarios and _Dict collections as shown by demo fiddle #1 here.

One way to fix this would be to serialize just _Dict (assuming that all scenarios are in the dictionary). Another would be to use PreserveReferencesHandling, e.g. by adding [JsonObject(IsReference = true)] to Scenario:

[JsonObject(IsReference = true)]
public class Scenario
{
    // Remainder unchanged
}

Notes:

  • There is no standard for serialization of references in JSON. Json.NET's implementation may not match that of other serializers.

  • PreserveReferencesHandling doesn't work for objects with parameterized constructors (see here for details), so make sure Scenario doesn't have one.

Demo fiddle #2 here showing everything working correctly with a default constructor, and #3 here with a parameterized constructor.

dbc
  • 104,963
  • 20
  • 228
  • 340