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.
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.
ReplyDeleteSo 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.
ReplyDelete@Peter Lanoie Thanks for the comment. Next time I will use more descriptive names for classes ;)
ReplyDeleteAs to "making the properties read/write". It should help to make A.Name writable. However, it is not always possible.
I've ran into an issue with default values and Preserve References, see https://github.com/JamesNK/Newtonsoft.Json/issues/1351
ReplyDeleteUpon deserialization you should use var result = JsonConvert.DeserializeObject<B>(serializedText, new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace });
ReplyDelete@Tyler Brinkley - Thanks, I didn't know this one.
ReplyDelete