18/08/2017

Json.net also tricked me

Home



Recently, I was tricked by Roslyn, today by Json.NET. My bloody luck ;) Let's look at the following two very simple classes. Class A has one readonly property and I had to define a special constructor to allow Json.NET to set this property. B is also simple. It has one property, this time of type A with some default value.

public class A
{
   public string Name { get; }

   [JsonConstructor]
   public A(string name) { Name = name; }
}

public class B
{
   public A Test { get; set; } = new A("Default");

}
Now let's serialize an instance of B in the following way:
var serializedText = JsonConvert.SerializeObject(new B() { Test = new A("Fun") { } });
In the result, as expected, we will get the following text:

{"Test":{"Name":"Fun"}}

Now let's deserialize this string:
var result = (B)JsonConvert.DeserializeObject(serializedText, typeof(B));
Now question for 1 million dollars. What will be the value of A.Test.Name in a deserialized object. If you say Fun you are wrong! It will be Default!

I was surprised and I spent some time investigating this issue. Why did it happen? Well, it seems to me that when Json.NET creates a new object during deserialization and it notices that some property of this object is not null, them this default value will not be overriden. If it's a problem, there are at least 2 solutions:
  • Remove a default value for this property. It doesn't matter if it is set in a constructor of via a property initializer.
  • Add a special constructor. For instance, for class B it'll look as below. This constructor instructs Json.NET that Test property must be set even if a default value exists.
public class B
{
   public A Test { get; set; } = new A("Default");

   public B() {}

   [JsonConstructor]
   public B(A test) { Test = test; }
}


*The picture at the beginning of the post comes from own resources and shows elephants from Warsaw zoo.

6 comments:

Peter Lanoie said...

Isn't the problem much simpler than property construction? A.Name is written as read only. Whether the deserializer creates an instance of A to assign to B.Test or uses an existing instance (your "Default") is irrelevant. B.Test.Name is still read-only.

Peter Lanoie said...

So I re-read your post several times (I think all the As and Bs were confusing me). I think I jumped the gun on my first comment before I fully understood the solution. More importantly, I don't think I've used the JsonConstructor attribute so I didn't realize what it was accomplishing. I've typically solve the problem you describe by just making the properties read/write.

Michał Komorowski said...

@Peter Lanoie Thanks for the comment. Next time I will use more descriptive names for classes ;)

As to "making the properties read/write". It should help to make A.Name writable. However, it is not always possible.

jogibear9988 said...

I've ran into an issue with default values and Preserve References, see https://github.com/JamesNK/Newtonsoft.Json/issues/1351

Anonymous said...

Upon deserialization you should use var result = JsonConvert.DeserializeObject<B>(serializedText, new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace });

Michał Komorowski said...

@Tyler Brinkley - Thanks, I didn't know this one.

Post a Comment