Reputation: 12458
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
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:
Integers will be returned as a long
(or a BigInteger
if larger than long.MaxValue
).
Floating point values will be returned as a double
or a decimal
, depending upon the setting of JsonSerializerSettings.FloatParseHandling
-- either FloatParseHandling.Double
or FloatParseHandling.Decimal
.
JSON strings that "look like" dates will get converted to DateTime
, DateTimeOffset
, or left as strings, depending on the setting JsonSerializerSettings.DateParseHandling
.
enum
names will be converted to strings.
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