Why can't Json.Net 6 deserialize a T as a generic list in a dynamic object? (worked in version 5)

844 Views Asked by At

I updated from version 5 to version 6.0.8 of Json.net. However, my code stopped working and it took me a while to understand that in previous version, it was able to handle the case where a generic type was instantiated with a list. But now, it doesn't. My question is, are there any settings I need to tweak to get the old behavior ?

In the old library, class

 public class Result<T> 
    {
        public T Data
        {
            get;
            set;
        }

        public int ResultCode
        {
            get;
            set;
        }
    }

Would work for a case where the class was instantiated with something like

Result<List<int>> result;

So in my code I was doing something like

dynamic result = JsonConvert.DeserializeObject(result, typeof(JObject));
int firstValue = (int)result.Data[0];

However, with the latest Json.Net, it fails and it throws an exception (Accessed JObject values with invalid key value: 0. Object property name expected.). The only work around I found is

var resultList = result.Data.ToObject<List<int>>();
int firstValue = resultList[0];

Obviously, this kind of defeat the purpose of dynamic and I'd rather go back to the old behavior. Anything I can do to tell Json.net that a T can be a List<int> ? I see that it has the metadata to know about it since the $type and $values property are visible in Result view of the dynamic object.

Any help is appreciated, thanks.

To help isolate the issue and answer the comment question, I created a test app. It turns out that the TypeNameHandling.All option is what causes the argument exception. I added comment in the code to show good/bad serialization. And yes, I think we need the All option in our code.

    using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ShowingBug
{
    public class InterestingResult
    {
        public string Information { get; set; }
    }

    public class Result<T> 
    {
        public T Data { get; set; }
        public int ResultCode { get; set; }
    }


    class Program
    {
        static void Main(string[] args)
        {
            var veryInteresting = new List<InterestingResult>();
            veryInteresting.Add(new InterestingResult() { Information = "Good" });

            Result<List<InterestingResult>> operationResult = new Result<List<InterestingResult>>();
            operationResult.Data = veryInteresting;
            operationResult.ResultCode = 1;

            JsonSerializerSettings serializerSettings = new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.Auto
            };

            var json = JsonConvert.SerializeObject(operationResult, serializerSettings);
            dynamic result = JObject.Parse(json);
            string information = (string)result.Data[0].Information;

            Console.Out.WriteLine(information);

            //The above works, however after some digging... 
            // I found that the option we use is not Auto, but All

            serializerSettings = new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.All
            };

            json = JsonConvert.SerializeObject(operationResult, serializerSettings);
            result = JObject.Parse(json);

            // Now, you get an ArgumentException from Json.net 6.0.8 on the following line
            information = (string)result.Data[0].Information; 

            // It worked in previous version of Json.Net
            // I'm using .net 4.5.2 (if that makes a difference)
        }
    }
}
1

There are 1 best solutions below

0
On

I don't see a bug here. The problem is that you seem to be expecting to get your original Result<T> object back even though you are specifying that the deserialized object type should be a JObject. That will never work because Result<T> is not a JObject, and a JObject cannot hold custom objects like Result<T>. A JObject can only hold a collection of JProperty objects, and those can only hold values derived from JToken. This is by design.

When you call JsonConvert.DeserializeObject(json, typeof(JObject)), that forces the return object to be a JObject, even if the receiving variable is declared as dynamic. Similarly, JObject.Parse() will always return a JObject.

If you want to get your original objects back, honoring the embedded type information in the JSON, you should be using the overload of JsonConvert.DeserializeObject() without a type, passing it the same JsonSerializationSettings that you used to serialize:

JsonSerializerSettings serializerSettings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All
};

dynamic result = JsonConvert.DeserializeObject(json, serializerSettings);