Serialization & Deserialization wont work with Dictionary as Backing field

Viewed 154

I've run into a weird issue with Serialization/Deserialization of a Dictionary when hidden in a backing field behind a property.

Here is the Fiddle: https://dotnetfiddle.net/RFZEur

End Result:

  • Language: C#
  • Framework: .Net Framework 4.8
  • Have a private backing field that has an combination of pairings with <int, string>
  • Can be Serialized into a list of strings (No ints included, pointing to the backing field referenced)
  • Cannot be Deserialized from the Serialized list -- the backing dictionary does not get populated.
public class SanityChecks
{
    private readonly ITestOutputHelper _testOutputHelper;
    public SanityChecks(ITestOutputHelper testOutputHelper)
    {
        _testOutputHelper = testOutputHelper;
    }
    public class TestClass 
    {
        [JsonIgnore]
        public Dictionary<int,string> _prvList = new Dictionary<int, string>();
        public IEnumerable<string> ListValues
        {
            get => _prvList.Select(p=> p.Value).ToList();
            set
            {
                var valArr = value.ToArray();
                for (var x = 0; x < valArr.Length; x++)
                {
                    _prvList.Add(x,valArr[x]);
                }
            }
        }
    }
    [Fact]
    public void SanityCheck_CanDeserialize()
    {
        var assumption = "{\"ListValues\":[\"TestValue\",\"AAA\"]}";
        var actual = JsonConvert.DeserializeObject<TestClass>(assumption);
        Assert.Equal(2, actual._prvList.Count());
        Assert.Equal(2, actual.ListValues.Count());
    }
    [Fact]
    public void SanityCheck_CanSerialize()
    {
        var assumption = new TestClass() { ListValues = new[] { "TestValue", "AAA" } };
        var actualSerialized = JsonConvert.SerializeObject(assumption);
        _testOutputHelper.WriteLine(actualSerialized);
        Assert.Equal("{\"ListValues\":[\"TestValue\",\"AAA\"]}", actualSerialized);
    }
    [Fact]
    public void SanityCheck_CanDeserializeFromSerialized()
    {
        var assumption = new TestClass() { ListValues = new[] { "TestValue", "AAA" } };
        var actualSerialized = JsonConvert.SerializeObject(assumption);
        _testOutputHelper.WriteLine(actualSerialized);
        var actualDeserialized = JsonConvert.DeserializeObject<TestClass>(actualSerialized);
        Assert.Equal(2, actualDeserialized._prvList.Count());
        var actualDeserializedSerialized = JsonConvert.SerializeObject(actualDeserialized);
        _testOutputHelper.WriteLine(actualDeserializedSerialized);
        Assert.Equal(actualSerialized, actualDeserializedSerialized);
    }
}

If you have some advice on how to retrieve this result, I'm open. I'm using XUnit for testing purposes, however the fiddle has a quick implementation of the tests below with slight modifications to make it a console application.

I've attempted with an implementation of ISerializable into the object, however I ran into the same issue.

Noted Weirdness: The removal of the Get Clause within the IEnumerable causes the deserialization to work (the serialization no longer works)

Edit: For additional clarity, I need the mapping of int,string pairings to be serialized as a list of strings, and I need that same serialized version to be deserializable as a collection of int,string pairings.

For the Fiddle: There should be no exceptions thrown For the XUnit: All tests should pass.

1 Answers

The reason that the ListValues enumerable is empty after deserialization is due to the following factors:

  1. By default, Json.Net will reuse existing object values during deserialization rather than create new ones. So, for properties such as ListValues, it will call the accessor first, and, finding an existing list, it will then proceed to populate it from the JSON.
  2. The accessor of ListValues doesn't return the same list instance each time; it always creates a new instance from the _prvList backing field.

As a result, here is what happens during deserialization:

  1. The serializer calls the ListValues accessor, which returns a new empty list created from the dictionary.
  2. The serializer populates that list from the JSON.
  3. The populated list is thrown away (the serializer assumes that the TestClass instance already has a reference to the list so it never calls the mutator).
  4. When the test later calls the ListValues accessor, it returns a new empty list again created from the dictionary.

You can change the behavior of the serializer by setting the ObjectCreationHandling setting to Replace. Applying this setting during deserialization allows your tests to pass. Fiddle here.

Related