Archives

Archives / 2019 / October
  • JSON Serialization in .NET Core 3: Tiny Difference, Big Consequences

    Recently, I migrated a web API from .NET Core 2.2 to version 3.0 (following the documentation by Microsoft). After that, the API worked fine without changes to the (WPF) client or the controller code on the server – except for one function that looked a bit like this (simplified naming, obviously):

    [HttpPost]
    public IActionResult SetThings([FromBody] Thing[] things)
    {
        ...
    }

    The Thing class has an Items property of type List<Item>, the Item class has a SubItems property of type List<SubItem>.

    What I didn’t expect was that after the migration, all SubItems lists were empty, while the Items lists contained, well, items.

    But it worked before! I didn’t change anything!

    In fact, I didn’t touch my code, but something else changed: ASP.NET Core no longer uses Json.NET by NewtonSoft. Instead, JSON serialization is done by classes in the new System.Text.Json namespace.

    The Repro

    Here’s a simple .NET Core 3.0 console application for comparing .NET Core 2.2 and 3.0.

    The program creates an object hierarchy, serializes it using the two different serializers, deserializes the resulting JSON and compares the results (data structure classes not shown yet for story-telling purposes):

    class Program
    {
        private static Item CreateItem()
        {
            var item = new Item();
            item.SubItems.Add(new SubItem());
            item.SubItems.Add(new SubItem());
            item.SubItems.Add(new SubItem());
            item.SubItems.Add(new SubItem());
            return item;
        }
        static void Main(string[] args)
        {
            var original = new Thing();
            original.Items.Add(CreateItem());
            original.Items.Add(CreateItem());
    
    
            var json = System.Text.Json.JsonSerializer.Serialize(original);
            var json2 = Newtonsoft.Json.JsonConvert.SerializeObject(original);
            Console.WriteLine($"JSON is equal: {String.Equals(json, json2, StringComparison.Ordinal)}");
            Console.WriteLine();
    
            var instance1 = System.Text.Json.JsonSerializer.Deserialize<Thing>(json);
            var instance2 = Newtonsoft.Json.JsonConvert.DeserializeObject<Thing>(json);
    
            Console.WriteLine($".Items.Count: {instance1.Items.Count} (System.Text.Json)");
            Console.WriteLine($".Items.Count: {instance2.Items.Count} (Json.NET)");
            Console.WriteLine();
            Console.WriteLine($".Items[0].SubItems.Count: {instance1.Items[0].SubItems.Count} (System.Text.Json)");
            Console.WriteLine($".Items[0].SubItems.Count: {instance2.Items[0].SubItems.Count} (Json.NET)");
        }
    }

    The program writes the following output to the console:

    JSON is equal: True
    
    .Items.Count: 2 (System.Text.Json)
    .Items.Count: 2 (Json.NET)
    
    .Items[0].SubItems.Count: 0 (System.Text.Json)
    .Items[0].SubItems.Count: 4 (Json.NET)
    

    As described, the sub-items are missing after deserializing with System.Text.Json.

    The Cause

    Now let’s take a look at the classes for the data structures:

    public class Thing
    {
        public Thing()
        {
            Items=new List<Item>();
        }
    
        public List<Item> Items { get; set; }
    }
    
    public class Item
    {
        public Item()
        {
            SubItems = new List<SubItem>();
        }
    
        public List<SubItem> SubItems { get; }
    }
    
    public class SubItem
    {
    }

    There’s a small difference between the two list properties:

    • The Items property of class Thing has a getter and a setter.
    • The Subitems property of class Item only has a getter.

    (I don’t even remember why one list-type property does have a setter and the other does not)

    Apparently, Json.NET determines that while it cannot set the SubItems property directly, it can add items to the list (because the property is not null).

    The new deserialization in .NET Core 3.0, on the other hand, does not touch a property it cannot set.

    I don’t see this as a case of “right or wrong”. The different behaviors are simply the result of different philosophies:

    • Json.NET favors “it just works.”
    • System.Text.Json works along the principle “if the property does not have a setter, there is probably a reason for that.”

    The Takeways

    1. Replacing any non-trivial library “A” with another library “B” comes with a risk.
    2. Details. It’s always the details.
    3. Consistency in your code increases the chances of consistent behavior when something goes wrong.