1

Following the official documentation:

string jsonTypeNameAll = JsonConvert.SerializeObject(stockholder, Formatting.Indented, new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All
});

Console.WriteLine(jsonTypeNameAll);
// {
//   "$type": "Newtonsoft.Json.Samples.Stockholder, Newtonsoft.Json.Tests",
//   "FullName": "Steve Stockholder",
//   "Businesses": {
//     "$type": "System.Collections.Generic.List`1[[Newtonsoft.Json.Samples.Business, Newtonsoft.Json.Tests]], mscorlib",
//     "$values": [
//       {
//         "$type": "Newtonsoft.Json.Samples.Hotel, Newtonsoft.Json.Tests",
//         "Stars": 4,
//         "Name": "Hudson Hotel"
//       }
//     ]
//   }
// }

I have copy pasted this code.

    public static string Convert<T>(T cacheObject)
    {
        return JsonConvert.SerializeObject(cacheObject, Formatting.Indented, new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.All
        });
    }

However, if I call it with Convert(DateTime.Now) , I get a serialized DateTime string, without the type included:

enter image description here

What am I doing wrong?

yesman
  • 7,165
  • 15
  • 52
  • 117
  • Looks like this might be similar: https://stackoverflow.com/questions/38859074/why-does-json-net-not-include-type-for-the-root-object-when-typenamehandling-is The update in the question might provide a workaround. – sr28 May 14 '20 at 10:41
  • Slightly different problem, as my object T is known at compile time, and I don't inherit from anything like the Dog class in his example. – yesman May 14 '20 at 10:59
  • Could you use a [Custom SerializationBinder](https://www.newtonsoft.com/json/help/html/SerializeSerializationBinder.htm) to display the types? – sr28 May 14 '20 at 13:03
  • Json.NET stores type information in the properties of objects, but `DateTime` is serialized as a primitive string and so there's no opportunity to store type information. You will need to encapsulate your primitive in some sort of wrapper object such as the ones from [this answer](https://stackoverflow.com/a/38340375/3744182) to [Deserialize Dictionary with enum values in C#](https://stackoverflow.com/q/38336390/3744182) or [this answer](https://stackoverflow.com/q/38777588/3744182) to [JSON.net (de)serialize untyped property](https://stackoverflow.com/q/38777588/3744182). – dbc May 19 '20 at 16:02

1 Answers1

5

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.

dbc
  • 104,963
  • 20
  • 228
  • 340