Serializing and deserializing TimeSpan values to a property of type Object

20,394

Json.Net has a TypeNameHandling setting for dealing with unknown types such that they can be deserialized properly. When this setting is turned on, it causes Json.Net to insert special $type properties into the JSON which are then used as hints when deserializing. Unfortunately, this setting does not seem to work with "simple" types such as TimeSpan, because their values are serialized as strings rather than objects.

To work around this issue, I would suggest making a custom JsonConverter that uses the same idea. Instead of outputting the string value of the object directly, the converter would output a sub-object representation with two properties: type and value. The type property of the sub-object would contain the assembly-qualified type name for the object, while the value property would contain the actual serialized value. On deserialization, the converter would look for the type property to know what type of object to instantiate from the value property. The nice thing about this approach is you do not have to add any additional properties or logic to your model class(es).

Here is how it might look in code:

class UnknownObjectConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return true;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JObject jo = new JObject();
        jo["type"] = value.GetType().AssemblyQualifiedName;
        jo["value"] = JToken.FromObject(value, serializer);
        jo.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jo = JObject.Load(reader);
        Type type = Type.GetType(jo["type"].ToString(), throwOnError: true);
        return jo["value"].ToObject(type, serializer);
    }
}

To use the converter, just decorate any object properties in your classes that need special handling with a [JsonConverter] attribute like this:

public class MyObject
{
    [JsonConverter(typeof(UnknownObjectConverter))]
    public object MyValue { get; set; }
}

Here is a round-trip demo which shows how this can work for several different types.

class Program
{
    static void Main(string[] args)
    {
        List<MyObject> list = new List<MyObject>
        {
            new MyObject { MyValue = TimeSpan.FromDays(2) },
            new MyObject { MyValue = "foo" },
            new MyObject { MyValue = new DateTime(2014, 12, 20, 17, 06, 44) },
            new MyObject { MyValue = new Tuple<int, bool>(23, true) }
        };

        string json = JsonConvert.SerializeObject(list, Formatting.Indented);
        Console.WriteLine(json);
        Console.WriteLine();

        list = JsonConvert.DeserializeObject<List<MyObject>>(json);
        foreach (MyObject obj in list)
        {
            Console.WriteLine(obj.MyValue.GetType().Name + ": " + obj.MyValue.ToString());
        }
    }
}

Here is the output:

[
  {
    "MyValue": {
      "type": "System.TimeSpan, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
      "value": "2.00:00:00"
    }
  },
  {
    "MyValue": {
      "type": "System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
      "value": "foo"
    }
  },
  {
    "MyValue": {
      "type": "System.DateTime, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
      "value": "2014-12-20T17:06:44"
    }
  },
  {
    "MyValue": {
      "type": "System.Tuple`2[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
      "value": {
        "Item1": 23,
        "Item2": true
      }
    }
  }
]

TimeSpan: 2.00:00:00
String: foo
DateTime: 12/20/2014 5:06:44 PM
Tuple`2: (23, True)
Share:
20,394
Jeff Dege
Author by

Jeff Dege

Updated on December 21, 2020

Comments

  • Jeff Dege
    Jeff Dege over 3 years

    I have a class that has a field of type Object that may contain a TimeSpan object.

    I am serializing this and the deserializing this:

    public class MyObject
    {
        public object myTimeSpan { get; set; }
    }
    
    ...
    
    var myObject = new MyObject { myTimeSpan = TimeSpan.FromDays(2) };
    var json = JsonConvert.SerializeObject(myObject);
    
    ...
    
    var duplicateObject = JsonConvert.DeserializeObject<MyObject>(json);
    

    And when I do, duplicateObject.myTimeSpan contains the string "2:00:00".

    How can I get this to deserialize into a TimeSpan object?

    Two points:

    1. I'm not dealing with strings that have been serialized somewhere else. The only strings I'll be deserializing are strings that I serialized.
    2. The field has to be of type object - it might contain a string, or a DateTime, or it might contain a TimeSpan. Currently, it's only the TimeSpan I'm having problems with.