2
votes

I reflected the JSON.NET JavaScriptDateTimeConverter class code, copied it, and renamed the class AS3DateTimeConverter so that I could modify it to format DateTime objects in a more precise and strongly-typed manor.

I have it output a type according to how JSON.NET outputs strongly-typed objects like so: {"$type":"System.DateTime, mscorlib","ticks":0}

The overridden WriteJson method of the JsonConverter runs to produce that value.

However, when I try to deserialize the string using the exact same settings with the same converter, the overridden ReadJson method never gets a chance to run and construct a DateTime from the ticks property, because the following errors occurs:

Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.DateTime' because the type requires a JSON primitive value (e.g. string, number, boolean, null) to deserialize correctly.

To fix this error either change the JSON to a JSON primitive value (e.g. string, number, boolean, null) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.

Path 'ticks', line 1, position 45.

Is this some kind of bug or limitation, which will not allow me to revive a DateTime type because it is a value-type? Or am I missing something?

Here are the serialization settings:

    JsonSerializerSettings settings = new JsonSerializerSettings();
    settings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
    settings.PreserveReferencesHandling = PreserveReferencesHandling.All;
    settings.ObjectCreationHandling = ObjectCreationHandling.Replace;
    settings.ConstructorHandling = ConstructorHandling.Default;
    settings.TypeNameHandling = TypeNameHandling.All;
    settings.TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple;
    settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
    settings.DateParseHandling = DateParseHandling.DateTime;
    settings.Converters.Add( new AS3DateTimeConverter() );
    //settings.Binder = new AS3SerializationBinder();
    string s = JsonConvert.SerializeObject( new DateTime( 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc ), settings );
    object o = JsonConvert.DeserializeObject( s, settings ); //s = "{\"$type\":\"System.DateTime, mscorlib\",\"ticks\":0}" //ERROR OCCURS HERE
2
This won't actually be storing ticks, it will store milliseconds elapsed since Jan 1, 1970, perhaps along with ticks for better round-trip precision, but the point is that the JsonConverter's overridden ReadJson method never even gets a chance to run, presumably because DateTime is a value type instead of a class? - Triynko
Can you show your converter code? - Brian Rogers
I could, but it wouldn't be worth anything. It simply extends the built-in JsonConverter and overrides WriteJson and ReadJson methods. The WriteJson method runs fine and produces the JSON string I displayed. The problem is that despite the string including "$type":"System.DateTime", the converter's CanConvert method is never called with that type, and the converter's ReadJson method is never called. It's as though it's not even trying to use my converter or even testing to see if it can be used. Perhaps it's a binding issue, but I thought that was automatic. - Triynko
I also tried modifying the WriteJson method to write the object as a custom reference-type class called "DateTimeWrapper" that just stores a single integer "ticks", so the string {"$type":"mynamespace.DateTimeWrapper","ticks":0} is produced by the serialization, but oddly enough, upon attempting to deserialize the string, the CanConvert method of my JsonConverter is called, but it receives an integer type instead of the mynamespace.DateTimeWrapper type. I find that odd. It's as thought it's ignoring the "$type" embedded in the JSON object string during deserialization. - Triynko
I was just able to reproduce the issue. You're right, the converter doesn't get called if you're just trying to deserialize a bare DateTime. But, if you wrap the DateTime in another object, it works. - Brian Rogers

2 Answers

0
votes

The problem seems to have something to do with deserializing a bare date. When the Date is wrapped in another object, it seems to work. This code works for me:

public class Program
{
    public static void Main(string[] args)
    {
        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
        settings.PreserveReferencesHandling = PreserveReferencesHandling.All;
        settings.ObjectCreationHandling = ObjectCreationHandling.Replace;
        settings.ConstructorHandling = ConstructorHandling.Default;
        settings.TypeNameHandling = TypeNameHandling.All;
        settings.TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple;
        settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
        settings.DateParseHandling = DateParseHandling.DateTime;
        settings.Converters.Add(new AS3DateTimeConverter());

        TestObject obj = new TestObject { Date = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) };
        string s = JsonConvert.SerializeObject(obj, settings);
        Console.WriteLine(s);
        object o = JsonConvert.DeserializeObject(s, settings);
        Console.WriteLine(((TestObject)o).Date.ToString());
    }
}

public class TestObject
{
    public DateTime Date { get; set; }
}

public class AS3DateTimeConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(DateTime);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JObject jo = new JObject();
        jo.Add("$type", "System.DateTime, mscorlib");
        jo.Add("ticks", ((DateTime)value).Ticks);
        jo.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jo = JObject.Load(reader);
        return new DateTime(jo["ticks"].Value<long>());
    }
}

Output:

{"$id":"1","$type":"Q20224027.TestObject, JsonTest","Date":{"$type":"System.DateTime, mscorlib","ticks":621355968000000000}}
1/1/1970 12:00:00 AM

UPDATE

To test the theory of whether converters get called for custom top level objects with embedded type information, I made a converter for the date wrapper object and serialized that instead. This worked, but only if I gave it a hint by using DeserializeObject<T> instead of DeserializeObject. Here is the code:

namespace Q20224027
{
    public class Program
    {
        public static void Main(string[] args)
        {
            JsonSerializerSettings settings = new JsonSerializerSettings();
            settings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
            settings.PreserveReferencesHandling = PreserveReferencesHandling.All;
            settings.ObjectCreationHandling = ObjectCreationHandling.Replace;
            settings.ConstructorHandling = ConstructorHandling.Default;
            settings.TypeNameHandling = TypeNameHandling.All;
            settings.TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple;
            settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
            settings.DateParseHandling = DateParseHandling.DateTime;
            settings.Converters.Add(new DateWrapperConverter());

            DateWrapper obj = new DateWrapper { Date = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) };
            string s = JsonConvert.SerializeObject(obj, settings);
            Console.WriteLine(s);
            object o = JsonConvert.DeserializeObject<DateWrapper>(s, settings);
            Console.WriteLine(((DateWrapper)o).Date.ToString());
        }
    }

    public class DateWrapper
    {
        public DateTime Date { get; set; }
    }

    public class DateWrapperConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(DateWrapper);
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            DateWrapper obj = (DateWrapper)value;
            JObject jo = new JObject();
            jo.Add("$type", typeof(DateWrapper).AssemblyQualifiedName);
            jo.Add("ticks", obj.Date.Ticks);
            jo.WriteTo(writer);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            return new DateWrapper { Date = new DateTime(jo["ticks"].Value<long>()) };
        }
    }
}

Output:

{"$type":"Q20224027.DateWrapper, JsonTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null","ticks":621355968000000000}
1/1/1970 12:00:00 AM
0
votes

Workaround I found.

If the object is wrapped in a list before being serialized, then it works ok, but only if you decorate the class with a JsonConverter attribute specifying the converter. Adding the converter to the list of converters for the serializers settings is insufficient.

For example, if you have a "Node" class which has a "Child" Node member (i.e. the type has members of it's own type), and you nest some nodes, then what I've found is that the converter is not called during serialization for anything but the top node when you only add the converter to the list of converters. On the other hand, if you explicitly decorate the class with the converter, then all the child nodes are run through the converter's WriteJson method as expected. So this basically renders the "Converters" collection of the serializer's settings non-functional.

When the objects are members of an array and their type is decorated with an explicit converter, then their converter's ReadJson and WriteJson methods are called when the types are encountered during serialization and deserialization.

When receiving a JSON string from a client, there are only two options to get it to work. Either you manually wrap the string in an object including a generic List "$type" and embed the received value as the sole value in the "$values" array, or you must just avoid all this and hard-code the expected recieved object type by calling typed DeserializeObject<T> method. What a mess.

The only reason I can fathom any of this making sense is if the DeserializeObject (non-generic) method was explicitly intended to NOT call converters for the top-level object, presumably so that it could be used in custom converter's WriteJson method without causing recursive calls to the converter. If that's the case, the design is terrible, because it leads to all the problems I've discussed.