2

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
Triynko
  • 18,766
  • 21
  • 107
  • 173
  • 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 Nov 26 '13 at 17:39
  • Can you show your converter code? – Brian Rogers Nov 26 '13 at 17:55
  • 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 Nov 26 '13 at 18:41
  • 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 Nov 26 '13 at 18:44
  • 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 Nov 26 '13 at 18:49
  • This guy is having the identical issue: http://stackoverflow.com/questions/14887389/json-net-not-calling-canconvert-for-collection-item – Triynko Nov 26 '13 at 18:50
  • Well, it's not working when wrapped in another object either. The $type is present, but it's not even trying to use my JsonConverter (i.e. it's not even calling CanConvert to see if it's appropriate for the type). It's just trying to create a bare-bones instance of the type all on its own. – Triynko Nov 26 '13 at 18:51
  • Hang on-- let me post the code I am using that works. You can try that. – Brian Rogers Nov 26 '13 at 18:52
  • Oh wait, unless you're saying that it's not processing the type correctly when the object is a top-level object in the string. In that case, this is either a bug or I'm not using the JsonConvert class right. – Triynko Nov 26 '13 at 18:56
  • Yes, that is what I'm saying-- if the `DateTime` is the top level, it doesn't work, but when it's not, then it does. So if your date will never be top level then you should be OK. – Brian Rogers Nov 26 '13 at 18:57
  • I think what's happening now is that the Converters list doesn't actually apply to objects, but rather it only applies to "members" of objects. I think this is some kind of major design flaw. – Triynko Nov 26 '13 at 19:00
  • In other words, the top-level object can be strongly typed, but it cannot be processed by a converter. It will be instantiated as a strongly-typed object, and it's members will be run through converters, but the top-level object itself will not. That means that the top-level object can never be a type that needs processed by a converter. – Triynko Nov 26 '13 at 19:02
  • I'm not sure if I would go that far-- have you tried it with a custom type? It may just be DateTime that is the issue. – Brian Rogers Nov 26 '13 at 19:03
  • Yes, I've tried it with a custom type. Same issue as the other guy was having, and actually what I just said doesn't explain his issue. His "SantaClause" objects are already in an array, so they are not top-level, and he's experiencing the same issue of the CanConvert not even being called for each object. – Triynko Nov 26 '13 at 19:04

2 Answers2

0

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
Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • That works, but again, the top-level class will never have a converter called. You could confirm the behavior by adding another converter for type 'TestObject' and running the same code. It will never call the CanConvert method of TestObjectConverter. – Triynko Nov 26 '13 at 19:07
  • I'm starting to wonder if it's an assembly resolution issue. Would it make a difference that my AS3DateTimeConverter is in a different assembly from the main application? It's a static reference, so it shouldn't be an issue, although I noticed that it fails to find the type unless I embed the assembly name in the $type. – Triynko Nov 26 '13 at 19:17
  • If it's in a different assembly, then you probably do need to embed the assembly name in the `$type` in the same way you did for `DateTime` (the `mscorlib` part). – Brian Rogers Nov 26 '13 at 19:21
  • Nope, that's not the problem either. I used your code and performed the test again with everything in a single assembly, and the issue is still there with top-level objects not being DEserialized with their assigned converters, despite being serialized with them. – Triynko Nov 26 '13 at 19:30
  • I created a converter for the TestObject wrapper, and once again I find that although the converter's WriteJson method is called during serialization and the type is propertly embedded, the converter is ignored upon deserialization and neither it's CanConvert nor it's ReadJson methods are called. – Triynko Nov 26 '13 at 19:34
  • Hmm, it is working for me. What are you doing differently than I am? I updated my answer. – Brian Rogers Nov 26 '13 at 19:35
  • Oh, it runs fine, but put a breakpoint in your ReadJson method... bet it doesn't get hit. It's constructing the TestObject on it's own during deserialization via default constructor and assignment, rather than through the ReadJson method of the converter. – Triynko Nov 26 '13 at 19:37
  • Oh, crap, you're right-- I just noticed the date isn't right in the output! I retract my statemtent. – Brian Rogers Nov 26 '13 at 19:38
  • What you'd get is probably the current date, because the bare-bones instance gets a default value. Had your settings disabled the "ignore extra members" or something like that, you'd also get an error. So I'm going to go as far as saying that top-level objects cannot use converters, and this is some kind of bug during deserialization. – Triynko Nov 26 '13 at 19:40
  • You can get it to work if you give it a hint by using `DeserializeObject` instead of `DeserializeObject`. – Brian Rogers Nov 26 '13 at 19:43
  • Right, I noticed that too. But I didn't consider it a proper workaround, because when receiving a JSON string from a client, one can't just assume a particular type (or at least shouldn't have to) as the $type should be embedded. My system is designed for seamless communication between AS3 and .NET, and I even have binding set up in web.config to map AS3 types to .NET types. This all works great when using plain old objects, but when certain incompatible types like Date>DateTime or ByteArray>System.Byte[] require special work, then this becomes an issue for those types as top-level objects. – Triynko Nov 26 '13 at 19:44
  • And that's the problem... having to hard-code the expected type breaks the whole feature of embedding the $type in the string for later deserialization. – Triynko Nov 26 '13 at 19:52
  • Understood. Perhaps you should report this to James Newton-King and see what he has to say about it. Or, you could try modifying the source code yourself and try to create a fix, then send a pull request. – Brian Rogers Nov 26 '13 at 19:56
  • Funny you should say that, lol, see: https://www.codeplex.com/site/users/view/JamesNK -- JamesNK Personal Statement: "If you have a question about a project and you haven't been able to find the answer in any documentation, post the question on Stackoverflow. I won't reply to support emails directly. Yes, this includes you." Anyway, I posted a bug report that links back to this post. – Triynko Nov 27 '13 at 21:06
  • LOL, well, I guess you've done what you can then! Good luck to you. – Brian Rogers Nov 27 '13 at 22:45
0

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.

Triynko
  • 18,766
  • 21
  • 107
  • 173