Search

DataContractJsonSerializer does not properly handle DateTime values by mattj1856

Closed
as Deferred Help for as Deferred

4
0
Sign in
to vote
Type: Bug
ID: 723368
Opened: 2/4/2012 12:02:05 PM
Access Restriction: Public
Moderator Decision: Sent to Engineering Team for consideration
1
Workaround(s)
1
User(s) can reproduce this bug
The DataContractJsonSerializer does not properly handle DateTime values. It blatantly disregards the precedent that has been established in the rest of .Net framework for proper use of the .Kind property.

More importantly, it completely disregards any offset passed in, choosing UTC if no offset, and Local time if ANY offset. This is very bad, because the offset and the date value are related, and ignoring the offset can provide the wrong date!

Serialization:

- A DateTime with a DateTimeKind.Local should be serialized to JSON with the local offset. This is working ok.

- A DateTime with a DateTimeKind.Utc should be serialized to JSON without an offset. This is working ok.

*** There should be some way to indicate how a DateTime with a DateTimeKind.Unspecified should be serialized to JSON. It currently assumes this is to be treated as local time, which might not be the case.

Deserialization:

- A JSON date without an offset should be treated as DateTimeKind.Utc. This is working ok.

*** A JSON date with an offset should be treated as DateTimeKind.Unspecified, and the provided offset should be applied. Instead, it is using as DateTimeKind.Local and applying the local offset.

*** A JSON date with an invalid offset (not in +HHMM or -HHMM form) should throw an exception. This is not occurring.

This should be raised as a critical issue. Anyone using the DataContractJsonSerializer for its intended use will run into these problems in any kind of global web application.
Details (expand)

Visual Studio/Team Foundation Server/.NET Framework Tooling version

Visual Studio 2010 SP1

Steps to reproduce

The following console program in C# illustrates the issue. See also the Actual vs. Expected results.

using System;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;

static class Program
{
    static void Main()
    {
        DisplaySerialization(new DateTime(2012, 1, 1, 0, 0, 0, DateTimeKind.Utc));
        DisplaySerialization(new DateTime(2012, 1, 1, 0, 0, 0, DateTimeKind.Local));
        DisplaySerialization(new DateTime(2012, 1, 1, 0, 0, 0, DateTimeKind.Unspecified));

        Console.WriteLine();

        DisplayDeserialization(@"""\/Date(1325376000000)\/""");
        DisplayDeserialization(@"""\/Date(1325376000000+0000)\/""");
        DisplayDeserialization(@"""\/Date(1325376000000-0000)\/""");
        DisplayDeserialization(@"""\/Date(1325376000000-0700)\/""");
        DisplayDeserialization(@"""\/Date(1325376000000-0500)\/""");
        DisplayDeserialization(@"""\/Date(1325376000000+BLAH)\/""");
        
        Console.ReadLine();
    }

    static void DisplaySerialization(DateTime dateTime)
    {
        var input = string.Format("{0:o} ({1})", dateTime, dateTime.Kind);
        var output = dateTime.ToJson();
        Console.WriteLine("{0} => {1}", input.PadRight(41), output);
    }

    static void DisplayDeserialization(string input)
    {
        var dateTime = input.FromJson<DateTime>();
        var output = string.Format("{0:o} ({1})", dateTime, dateTime.Kind);
        Console.WriteLine("{0} => {1}", input.PadRight(30), output);
    }

    static string ToJson(this object obj)
    {
        using (var ms = new MemoryStream())
        using (var reader = new StreamReader(ms, Encoding.UTF8))
        {
            var ser = new DataContractJsonSerializer(obj.GetType());
            ser.WriteObject(ms, obj);
            ms.Seek(0, SeekOrigin.Begin);
            return reader.ReadToEnd();
        }
    }

    static T FromJson<T>(this string json)
    {
        var bytes = Encoding.UTF8.GetBytes(json);
        using (var ms = new MemoryStream(bytes))
        {
            var ser = new DataContractJsonSerializer(typeof(T));
            return (T)ser.ReadObject(ms);
        }
    }
}

Product Language

English

Operating System

Windows 7

Operating System Language

English

Actual results

2012-01-01T00:00:00.0000000Z (Utc)         => "\/Date(1325376000000)\/"
2012-01-01T00:00:00.0000000-07:00 (Local) => "\/Date(1325401200000-0700)\/"
2012-01-01T00:00:00.0000000 (Unspecified) => "\/Date(1325401200000-0700)\/"

"\/Date(1325376000000)\/"     => 2012-01-01T00:00:00.0000000Z (Utc)
"\/Date(1325376000000+0000)\/" => 2011-12-31T17:00:00.0000000-07:00 (Local)
"\/Date(1325376000000-0000)\/" => 2011-12-31T17:00:00.0000000-07:00 (Local)
"\/Date(1325376000000-0700)\/" => 2011-12-31T17:00:00.0000000-07:00 (Local)
"\/Date(1325376000000-0500)\/" => 2011-12-31T17:00:00.0000000-07:00 (Local)
"\/Date(1325376000000+BLAH)\/" => 2011-12-31T17:00:00.0000000-07:00 (Local)

Expected results

2012-01-01T00:00:00.0000000Z (Utc)         => "\/Date(1325376000000)\/"
2012-01-01T00:00:00.0000000-07:00 (Local) => "\/Date(1325401200000-0700)\/"
2012-01-01T00:00:00.0000000 (Unspecified) => "\/Date(1325401200000+????)\/"

"\/Date(1325376000000)\/"     => 2012-01-01T00:00:00.0000000 (Utc)
"\/Date(1325376000000+0000)\/" => 2012-01-01T00:00:00.0000000Z (Unspecified)
"\/Date(1325376000000-0000)\/" => 2012-01-01T00:00:00.0000000Z (Unspecified)
"\/Date(1325376000000-0700)\/" => 2011-12-31T17:00:00.0000000-07:00 (Unspecified)
"\/Date(1325376000000-0500)\/" => 2011-12-31T19:00:00.0000000-05:00 (Unspecified)
"\/Date(1325376000000+BLAH)\/" => (an exception should be thrown)
File Attachments
0 attachments
Sign in to post a comment.
Posted by Microsoft on 2/6/2012 at 1:40 PM
Thanks for your feedback. We have a bug tracking fixing this in a future release - it is unlikely to happen for Dev11/.Net 4.5 though. Thanks.
Posted by mattj1856 on 2/6/2012 at 11:43 AM
After further research, I discovered that the "number of milliseconds since 1/1/1970" that is part of this format represents the "Unix Epoch" time, which is indeed in UTC. I therefore suggest that the current implementation of a timestamp without an offset be treated as UTC, where a timestamp of +0000 or -0000 should be "unspecified", as it might represent UTC, but might also represent London standard time (for example). I have update the bug and samples accordingly.

The remainder of the issues are as were originally stated. The biggest being that the presence of an offset is triggering local time without taking the offset into account.
Posted by MS-Moderator09 [Feedback Moderator] on 2/5/2012 at 6:02 PM
Thank you for submitting feedback on Visual Studio 2010 and .NET Framework. Your issue has been routed to the appropriate VS development team for review. We will contact you if we require any additional information.
Posted by MS-Moderator01 on 2/4/2012 at 12:45 PM
Thank you for your feedback, we are currently reviewing the issue you have submitted. If this issue is urgent, please contact support directly(http://support.microsoft.com)
Posted by mattj1856 on 2/4/2012 at 12:29 PM
A separate, but related issue is that the DateTimeOffset class should serialize/deserialize to this same format, since the Offset is already there. Instead, it serializes to something like:

{"DateTime":"\/Date(1325401200000)\/","OffsetMinutes":-420}

Sign in to post a workaround.
Posted by mattj1856 on 2/8/2012 at 11:07 AM
Workaround - use a different serializer. ServiceStack.Text works well. The default option has this bug fixed as of version 3.4.4, and there is also an option to keep the bug in place for exact parity with DCJS. There's even an option to use ISO-8601 standard date formats (which is proabably a good idea). The options are set in ServiceStack.Text via the JsConfig.DateHandler property.

DCJS should really fix this bug though. Not everyone will be able to take a dependency on a third-party component.