Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does the Newtonsoft deserializer not deserialize a smart getter?

Tags:

c#

json.net

Context

I have two classes:

  • LiteralModel is a wrapper around a string Value property.
  • Expression has a string Name and a LiteralModel called Literal.

    public class ExpressionModel
    {
        private string name;
        public string Name { get => name ?? ""; set => name = value; }
    
        private LiteralModel literal;
        public LiteralModel Literal
        {
            get => literal ?? new LiteralModel();
            set => literal = value;
        }
    }
    
    public class LiteralModel 
    {
        private string value;
        public string Value { get => value ?? ""; set => this.value = value; }
    }
    

All are public properties with public getters and setters, and so I would expect both of them to serialize and deserialize easily, even with the null guards, and, for the most part, they do.

The Problem

The Literal property of the ExpressionModel does not deserialize properly. Below is a minimal test that demonstrates the issue:

    public void TestNewtonsoftExpressionDeserialization()
    {
        ExpressionModel expression = new ExpressionModel
        {
            Name = "test",
            Literal = new LiteralModel { Value = "61" }
        };

        string json = JsonConvert.SerializeObject(expression);

        Assert.IsTrue(json.Contains("61")); // passes

        ExpressionModel sut = JsonConvert.DeserializeObject<ExpressionModel>(json);

        Assert.AreEqual("test", sut.Name); // passes
        Assert.AreEqual("61", sut.Literal.Value); // fails
    }

As you can see, the JSON looks how I want/expect, (wrapping the string "61"), but when I deserialize that back into an ExpressionModel, the Literal test fails--it gets a LiteralModel of an empty string.

What I've Tried

If I remove the smart-ness of the Literal getter of the expression model, it behaves as expected--all tests pass. But smart properties do work on the string properties. So why not on my LiteralModel object?

Even weirder, all tests pass if I move the null-check to the setter instead of the getter like so:

        public LiteralModel Literal
        {
            get => literal;
            set => literal = value ?? new LiteralModel();
        }

Conclusion

In short, nothing phases the serializer, and smart setters are fine, but smart getters break deserialization, except for string.

This seems like wildly arbitrary behavior. Does anyone know why this might be or if there's any way to get these classes to work as written?

like image 890
Eleanor Holley Avatar asked Jan 30 '20 16:01

Eleanor Holley


1 Answers

I suspect the problem is that Json.NET is calling your property getter, and then calling the setter on the result. For example, something like this - although obviously, via reflection:

var expression = new ExpressionModel();
expression.Name = "test";
var literal = expression.Literal;
if (literal is null)
{
    // No literal - create one and set it
    literal = new LiteralModel();
    expression.Literal = literal;
}
// Now literal is non-null either way, so set the value.
literal.Value = "61";

With the way your code works, your Literal getter is creating a new LiteralModel, but then throwing it away. Leaving Json.NET aside, that's still pretty confusing. For example:

var expression = new Expression();
expression.Literal.Value = "foo";
Console.WriteLine(expression.Literal.Value); // Empty string

You could change your ExpressionModel code to assign the newly-created LiteralModel to the property if it creates one:

public LiteralModel Literal
{
    get => literal ?? (literal = new LiteralModel());
    set => literal = value;
}

That will avoid the "simple code" behaviour being so confusing - and I expect it will fix the Json.NET behaviour too.

like image 52
Jon Skeet Avatar answered Oct 19 '22 12:10

Jon Skeet