Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Json.Net Deserialization Constructor vs. Property Rules

I was troubleshooting a (de)serialization issue with the following class using Json.Net:

public class CoinsWithdrawn
{
    public DateTimeOffset WithdrawlDate { get; private set; }
    public Dictionary<CoinType, int> NumberOfCoinsByType { get; private set; }

    public CoinsWithdrawn(DateTimeOffset withdrawDate, Dictionary<CoinType, int> numberOfCoinsByType)
    {
        WithdrawlDate = withdrawDate;
        NumberOfCoinsByType = numberOfCoinsByType;
    }
}

The problem is that the constructor argument "withdrawDate" is named differently than the property name "WithDrawlDate". Making the names match (even ignoring case) fixed the issue.

However, I wanted to understand this a little better, so I reverted the code and tested after making both the setters public. This also fixed the problem.

Finally, I switched from auto-properties to properties with backing fields so that I could fully debug and see what was actually going on:

public class CoinsWithdrawn
{
    private DateTimeOffset _withdrawlDate;
    private Dictionary<CoinType, int> _numberOfCoinsByType;

    public DateTimeOffset WithdrawlDate
    {
        get { return _withdrawlDate; }
        set { _withdrawlDate = value; }
    }

    public Dictionary<CoinType, int> NumberOfCoinsByType
    {
        get { return _numberOfCoinsByType; }
        set { _numberOfCoinsByType = value; }
    }

    public CoinsWithdrawn(DateTimeOffset withdrawDate, Dictionary<CoinType, int> numberOfCoinsByType)
    {
        WithdrawlDate = withdrawDate;
        NumberOfCoinsByType = numberOfCoinsByType;
    }
}

I tried this with and without a default constructor (code shown omits the default constructor).

With the default constructor: default constructor is called, then both property setters are called.

Without the default constructor: non-default constructor is called, then WithDrawlDate setter is called. NumberOfCoinsByType setter is never called.

My best guess is that the deserializer is keeping track of which properties can be set via the constructor (by some convention, since casing seems to be ignored), and then uses property setters where possible to fill in the gaps.

Is this the way it works? Are the order of operations/rules for deserialization documented somewhere?

like image 508
Phil Sandler Avatar asked Oct 13 '15 16:10

Phil Sandler


1 Answers

My best guess is that the deserializer is keeping track of which properties can be set via the constructor (by some convention, since casing seems to be ignored), and then uses property setters where possible to fill in the gaps. Is this the way it works?

Yes, that's pretty much the gist. If you take a look at the source code you can see for yourself. In the JsonSerializerInternalReader class there is a method CreateObjectUsingCreatorWithParameters which handles the instantiation of objects using a non-default constructor. I've copied the relevant bits below.

The ResolvePropertyAndCreatorValues method grabs the data values from the JSON, then the loop tries to match them up to the constructor parameters. Those that don't match1 are added to the remainingPropertyValues dictionary. The object is then instantiated using the matched parameters, with null/default values used to fill any gaps. A second loop later in the method (not shown here) then attempts to call setters on the object for the remaining properties in this dictionary.

IDictionary<JsonProperty, object> propertyValues = 
    ResolvePropertyAndCreatorValues(contract, containerProperty, reader, objectType, out extensionData);

object[] creatorParameterValues = new object[contract.CreatorParameters.Count];
IDictionary<JsonProperty, object> remainingPropertyValues = new Dictionary<JsonProperty, object>();

foreach (KeyValuePair<JsonProperty, object> propertyValue in propertyValues)
{
    JsonProperty property = propertyValue.Key;

    JsonProperty matchingCreatorParameter;
    if (contract.CreatorParameters.Contains(property))
    {
        matchingCreatorParameter = property;
    }
    else
    {
        // check to see if a parameter with the same name as the underlying property name exists and match to that
        matchingCreatorParameter = contract.CreatorParameters.ForgivingCaseSensitiveFind(p => p.PropertyName, property.UnderlyingName);
    }

    if (matchingCreatorParameter != null)
    {
        int i = contract.CreatorParameters.IndexOf(matchingCreatorParameter);
        creatorParameterValues[i] = propertyValue.Value;
    }
    else
    {
        remainingPropertyValues.Add(propertyValue);
    }

    ...
} 
...

object createdObject = creator(creatorParameterValues);

...

1The parameter matching algorithm is essentially a case insensitive search that falls back to being case sensitive if there are multiple matches found. Take a look at the ForgivingCaseSensitiveFind utility method if you're interested.

Are the order of operations/rules for deserialization documented somewhere?

Not to my knowledge. Official documentation is here, but it does not go into this level of detail.

like image 199
Brian Rogers Avatar answered Nov 04 '22 06:11

Brian Rogers