Property setter within Json deserialization for a List<Bar>

Viewed 171

In System.Text.Json how does the "setter" work when using properties that are of type List? it is not how i would expect, especially in relation to the setter for a string as example.

Here's what I mean:

public class Program
{
    public static void Main()
    {
        var json = @"
            {
              ""doot"": ""bloop bloop"",
              ""bars"": 
              [
                {
                    ""id"": 1
                },
                {
                    ""id"": 2
                }
              ]
            }
        ";
        
        var obj = JsonSerializer.Deserialize<Foo>(json);
        Console.WriteLine($"obj.Doot: {obj.Doot}");
        
        if (obj.Bars == null)
            Console.WriteLine("Bars is null.");
        else
            Console.WriteLine($"obj.Bars.Count: {obj.Bars.Count}");
        
    }
    
    public class Foo
    {
        private string _doot;
        [JsonPropertyName("doot")]
        public string Doot
        {
            get => _doot;
            set
            {
                Console.WriteLine($"Setting Doot to {value}");
                _doot = value;
            }
        }
        
        private List<Bar> _bars;
        [JsonPropertyName("bars")]
        public List<Bar> Bars 
        {
            get => _bars;
            set
            {
                Console.WriteLine($"Bars Value count during setter: {value.Count}");
                _bars = value;
            }
        }
    }
    public class Bar
    {
        [JsonPropertyName("id")]
        public int Id {get;set;}
    }
}

output:

Setting Doot to bloop bloop

Bars Value count during setter: 0

obj.Doot: bloop bloop

obj.Bars.Count: 2

In the above, when the Doot setter fires, value has the value being set to the backing field _doot. When the Bars setter fires, value has something in it, but it is not the two Bar instances, as shown by the Console.WriteLine statement within the setter, which reports a 0 count.

If I needed to do some logic based on the values being set within the property Bars, how can I accomplish that if value does not have the information?

fiddle for reference:

https://dotnetfiddle.net/7z2O9S

EDIT:

it was pointed out to me elsewhere (caschw if you're here thanks) that the list is likely initialized as an empty list, then list.Add or list.AddRange could be used after initialization (i'm unclear which is used under the covers of System.Text.Json); meaning the setter would only ever be done once unless I change the reference to the list. This makes sense, but still, how would I hook into running some logic whenever the collection of Bars is changed?

1 Answers

The assumption in your edit is correct. The way the list is deserialized is that first the list object is being created, and then the individual elements are added. If you think about how JSON works, then it makes sense for a parser and deserializer to work that way:

When the parser encounters a [ it knows that a JSON array is being started. So when that object is to be deserialized into a .NET collection type, then it can already create that collection at that moment. After all, the deserializer knows the target type it should deserialize to.

So the deserializer creates the list object, and then starts to deserialize the items inside. In your case, these are Bar objects. So it constructs those, sets the properties, and—once the object is completed—finally adds them to the list.

You can actually see this in the source. There are a number of collection converters, depending on what target type you have, but they all inherit from IEnumerableDefaultConverter which basically has the behavior I described above. For List<T>, the actual work then happens in the ListOfTConverter which basically just initializes a new list and calls Add for each item.

If I needed to do some logic based on the values being set within the property Bars, how can I accomplish that if value does not have the information?

I don’t think you should. You should see the deserialization process as a single operation and not interfere with it. I would suggest you to do that afterwards, e.g. as a post-processing step after deserialization.

Related