shenku
shenku

Reputation: 12458

Using Json.net to deserialize to any unkown type works for objects but not value types

I've implemented a Json Serializer based on Json.net to accept any object type and serialize it (for placement into my cache)

The cache interface doesn't allow me to speficy the type, so When I retrieve from the cache I need to dynamically create the type from the meta information.

Which works well for objects, the problem I am now facing is that i doesn't work for value types, I will get an exception saying something along the lines of cannot cast JValue to JObject.

My question is how can I cater for value types as well as object types? It would be great if there was a TryParse for a JObject, which I could write myself, but feel like I am going down a rabbit hole?

What is the best way to achieve this?

My code is as follows, settings for Json.net:

_settings = new JsonSerializerSettings
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver(),
                NullValueHandling = NullValueHandling.Ignore,
                DateTimeZoneHandling = DateTimeZoneHandling.Utc,
                TypeNameHandling = TypeNameHandling.All
            };

_settings.Converters.Add(new StringEnumConverter());

The set function (serialize):

public void Put(string cacheKey, object toBeCached, TimeSpan cacheDuration)
        {
            _cache.Set(cacheKey, JsonConvert.SerializeObject(toBeCached, _settings), cacheDuration);
        }

And the get (deserialize):

 public object Get(string cacheKey)
    {
        try
        {
            var value = _cache.Get(cacheKey);

            if (!value.HasValue)
            {
                return null;
            }

            var jobject = JsonConvert.DeserializeObject<JObject>(value);
            var typeName = jobject?["$type"].ToString();

            if (typeName == null)
            {
                return null;
            }

            var type = Type.GetType(typeName);
            return jobject.ToObject(type);
        }
        catch (Exception e)
        {
            // Todo
            return null;
        }
    }

Upvotes: 1

Views: 2080

Answers (1)

dbc
dbc

Reputation: 116980

You need to parse to a JToken rather than a JObject, then check to see if the returned type is a JValue containing a JSON primitive:

public static object Get(string value)
{
    var jToken = JsonConvert.DeserializeObject<JToken>(value);
    if (jToken == null)
        return null;
    else if (jToken is JValue)
    {
        return ((JValue)jToken).Value;
    }
    else
    {
        if (jToken["$type"] == null)
            return null;
        // Use the same serializer settings as used during serialization.
        // Ideally with a proper SerializationBinder that sanitizes incoming types as suggested
        // in https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_TypeNameHandling.htm
        var _settings = new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            NullValueHandling = NullValueHandling.Ignore,
            DateTimeZoneHandling = DateTimeZoneHandling.Utc,
            TypeNameHandling = TypeNameHandling.All,
            Converters = { new StringEnumConverter() },
            //SerializationBinder = new SafeSerializationBinder(),
        };
        // Since the JSON contains a $type parameter and TypeNameHandling is enabled, if we deserialize 
        // to type object the $type information will be used to determine the actual type, using Json.NET's
        // serialization binder: https://www.newtonsoft.com/json/help/html/SerializeSerializationBinder.htm
        return jToken.ToObject(typeof(object), JsonSerializer.CreateDefault(_settings));
    }
}

Note, however, that type information for primitives will not get precisely round-tripped:

If you need to round-trip type information for primitives, consider using TypeWrapper<T> from Deserialize specific enum into system.enum in Json.Net to encapsulate your root objects.

Finally, if there is any chance you might be deserializing untrusted JSON (and if you are deserializing from a file or from the internet then you certainly are), please note the following caution from the Json.NET documentation:

TypeNameHandling should be used with caution when your application deserializes JSON from an external source. Incoming types should be validated with a custom SerializationBinder when deserializing with a value other than None.

For a discussion of why this may be necessary, see TypeNameHandling caution in Newtonsoft Json, How to configure Json.NET to create a vulnerable web API, and Alvaro Muñoz & Oleksandr Mirosh's blackhat paper https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf

Upvotes: 1

Related Questions