2

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.

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
Seeds
  • 372
  • 2
  • 14
  • Very unclear what your problem is - thanks for providing [MCVE], but you still need to explain what you want to achieve... Code that doesn't do what you want can't alone show that (i.e. in this particular case it is very unclear how you expect read-only type to be deserialized). – Alexei Levenkov Jul 16 '20 at 19:26
  • @AlexeiLevenkov thanks! I'll add some clarity, i thought i was clear. I need a private pairing to be externally serialized as a list of strings (ignoring the int) AND deserialized from a list of strings into an pairing (generated INT based off index) Neither object is read-only. – Seeds Jul 16 '20 at 20:03
  • Take a look at [this answer](https://stackoverflow.com/a/34663716/3791245). The default serialization of `IEnumerable` uses `List`, but your test code builds _arrays_. Because of this, the JSON generated may include `"$type": "System.String[], mscorlib"` to show that it's an array type, and not a List. I haven't ran your tests myself, but I suspect this may be why it's failing, as your hard coded JSON strings don't include the array type. Another answer in that link suggests using the attribute `[JsonProperty(TypeNameHandling = TypeNameHandling.None)]` to avoid this. – Sean Skelly Jul 16 '20 at 20:53
  • @SeanSkelly I attempted the solution provided [JsonProperty(TypeNameHandling = TypeNameHandling.None)] Same result. I decided to try a few other interfaces as referenced in the documentation, same reproduced result :( Good read though! Looking at the referenced overflow page, it deals with a difference in behaviour from IList, List, and an array of objects due to the serialization into an array but an expected type being a List. According to the same documentation, it requires the IEnumerable implementation, within the defined object. – Seeds Jul 16 '20 at 21:11
  • `set { var valArr = value.ToArray(); for (var x = 0; x < valArr.Length; x++) { _prvList.Add(x,valArr[x]); } }` is very odd. It looks like you only allow the setter to be called once (since calling it twice with the same data will return in an exception of the second invocation due to `Add`)? And if the key is always an index why not use a `List` as the backing store (since it has an index built in)? – mjwills Jul 17 '20 at 00:43
  • @mjwills this was missed in migration to a minimum required replication of the issue. There is a clear step occurring in the actual code. Good eye! – Seeds Jul 17 '20 at 01:27

1 Answers1

2

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.

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • Thanks for the informative and extremely accurate answer (it also explains the weirdness stated above, and some other stuff not documented). I didn't use the solution provided verbatim, but instead i assigned an attribute to the ListValues property (still ObjectCreationHandling). Thanks for your help! – Seeds Jul 17 '20 at 01:45