TypeNameHandling
works by adding a special, reserved "$type"
property to JSON objects specifying the .Net type that was serialized. In addition, as explained in Newtonsoft's Serialization Guide, arrays will get nested inside a wrapper object that specifies the type:
Note that if TypeNameHandling or PreserveReferencesHandling has been enabled for JSON arrays on the serializer, then JSON arrays are wrapped in a containing object. The object will have the type name/reference properties and a $values property, which will have the collection data.
However, there is no such special case implemented when serializing JSON primitives. A .Net object that is serialized as a primitive will be emitted as a primitive without any wrapper added, even if TypeNameHandling
is specified.
Thus, if you want to specify .Net type information for JSON primitives, you must create a wrapper object yourself. Some examples can be found in this answer to Deserialize Dictionary<string, object> with enum values in C# or this answer to JSON.net (de)serialize untyped property.
Following those answers, create the following wrapper:
public abstract class TypeWrapper
{
// Taken from this answer https://stackoverflow.com/a/38340375/3744182
// To https://stackoverflow.com/questions/38336390/deserialize-dictionarystring-object-with-enum-values-in-c-sharp
// By https://stackoverflow.com/users/3744182/dbc
protected TypeWrapper() { }
[JsonIgnore]
public abstract object ObjectValue { get; }
public static TypeWrapper CreateWrapper<T>(T value)
{
if (value == null)
return new TypeWrapper<T>();
var type = value.GetType();
if (type == typeof(T))
return new TypeWrapper<T>(value);
// Return actual type of subclass
return (TypeWrapper)Activator.CreateInstance(typeof(TypeWrapper<>).MakeGenericType(type), value);
}
}
public sealed class TypeWrapper<T> : TypeWrapper
{
public TypeWrapper() : base() { }
public TypeWrapper(T value)
: base()
{
this.Value = value;
}
public override object ObjectValue { get { return Value; } }
public T Value { get; set; }
}
Then modify your Convert<T>()
as follows:
public static partial class JsonExtensions
{
static readonly IContractResolver globalResolver = new JsonSerializer().ContractResolver;
public static string Convert<T>(T cacheObject)
{
return JsonConvert.SerializeObject(ToTypeWrapperIfRequired(cacheObject), Formatting.Indented, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
});
}
public static T UnConvert<T>(string json)
{
var obj = JsonConvert.DeserializeObject<object>(json, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
});
if ((obj is TypeWrapper wrapper))
return (T)wrapper.ObjectValue;
return (T)obj;
}
static object ToTypeWrapperIfRequired<T>(T obj, IContractResolver resolver = null)
{
resolver = resolver ?? globalResolver;
if (obj == null)
return null;
// Type information is redundant for string or bool
if (obj is bool || obj is string)
return obj;
var contract = resolver.ResolveContract(obj.GetType());
if (contract is JsonPrimitiveContract)
return TypeWrapper.CreateWrapper(obj);
return obj;
}
}
And now you can convert to JSON and back as follows:
var json = JsonExtensions.Convert(obj);
var objBack = JsonExtensions.UnConvert<T>(json);
Notes:
A wrapper will be added to the object being serialized only if necessary -- i.e. if the .Net object is going to be serialized as a JSON primitive rather than a JSON object or array. In addition I don't add a wrapper if the incoming object is a string
or a bool
as these types can be inferred unambiguously from the JSON.
To do:
- Decide whether the type of null-valued nullable structs needs to be preserved. If so they will need wrapping also.
- Decide what should be done when round-tripping a
JValue
.
- Serializing and deserializing an already-wrapped object result in it being unwrapped on deserialization, so you may want to throw an exception from
Convert<T>()
if the incoming object is a wrapper.
Do be aware that using TypeNameHandling
can introduce security vulnerabilities into your application. For details see TypeNameHandling caution in Newtonsoft Json and External json vulnerable because of Json.Net TypeNameHandling auto?.
Also be aware that type name information isn't always portable between different .Net frameworks. See for example Serializing and deserializing in different frameworks #1378. You may need to write a custom ISerializationBinder
in such situations.
Demo fiddle here.