10

In regular .net, If we have a time that has DateTimeKind.Unspecified If we convert ToLocal -- it assumes the input date is UTC when converting. If we convert ToUniversal -- it assumes the input date is local when converting

However, in JSON.Net, if our string date in JSON.Net is unspecified, it doesn't seem to have this logic? Look at my test cases below - am I doing some thing wrong? Or is this by design? or a bug in JSON.Net? Thanks!

    // TODO: This Fails with output
    //      date string: "2014-06-02T21:00:00.0000000"
    //      date serialized: 2014-06-02T21:00:00.0000000Z
    //      Expected date and time to be <2014-06-03 04:00:00>, but found <2014-06-02 21:00:00>.
    [TestMethod]
    public void NEW_Should_deserialize_unspecified_datestring_to_utc_date()
    {
        string dateString = "\"2014-06-02T21:00:00.0000000\"";
        DateTime dateRaw = new DateTime(2014, 6, 2, 21, 0, 0, 0, DateTimeKind.Unspecified);
        DateTime dateRawAsUtc = new DateTime(2014, 6, 3, 4, 0, 0, 0, DateTimeKind.Utc);
        dateRawAsUtc.Should().Be(dateRaw.ToUniversalTime());

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
        settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
        DateTime dateSerialized = JsonConvert.DeserializeObject<DateTime>(dateString, settings);                

        Console.WriteLine("date string: " + dateString);
        Console.WriteLine("date serialized: " + dateSerialized.ToString("o"));

        dateSerialized.Kind.Should().Be(DateTimeKind.Utc); 
        dateSerialized.Should().Be(dateRaw.ToUniversalTime());
        dateSerialized.Should().Be(dateRawAsUtc);
    }

    // TODO: This Fails with output
    //      date string: "2014-06-02T21:00:00.0000000"
    //      date serialized: 2014-06-02T21:00:00.0000000-07:00
    //      Expected date and time to be <2014-06-02 14:00:00>, but found <2014-06-02 21:00:00>.
    [TestMethod]
    public void NEW_Should_deserialize_unspecified_datestring_to_local_date()
    {
        string dateString = "\"2014-06-02T21:00:00.0000000\"";
        DateTime dateRaw = new DateTime(2014, 6, 2, 21, 0, 0, 0, DateTimeKind.Unspecified);
        DateTime dateRawAsLocal = new DateTime(2014, 6, 2, 14, 0, 0, 0, DateTimeKind.Local);
        dateRawAsLocal.Should().Be(dateRaw.ToLocalTime());

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.DateTimeZoneHandling = DateTimeZoneHandling.Local;
        settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
        DateTime dateSerialized = JsonConvert.DeserializeObject<DateTime>(dateString, settings);

        Console.WriteLine("date string: " + dateString);
        Console.WriteLine("date serialized: " + dateSerialized.ToString("o"));

        dateSerialized.Kind.Should().Be(DateTimeKind.Local);
        dateSerialized.Should().Be(dateRaw.ToLocalTime());
        dateSerialized.Should().Be(dateRawAsLocal);
    }

    [TestMethod]
    public void NEW_Should_deserialize_unspecified_datestring_to_unspecified_date() 
    {
        string dateString = "\"2014-06-02T21:00:00.0000000\""; // unspecified, does not have the 'Z'
        DateTime dateRaw = new DateTime(2014, 6, 2, 21, 0, 0, 0, DateTimeKind.Unspecified);

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.DateTimeZoneHandling = DateTimeZoneHandling.Unspecified;
        settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
        DateTime dateSerialized = JsonConvert.DeserializeObject<DateTime>(dateString, settings);                

        Console.WriteLine("date string: " + dateString);
        Console.WriteLine("date serialized: " + dateSerialized.ToString("o"));
        dateSerialized.Kind.Should().Be(DateTimeKind.Unspecified); 
        dateSerialized.Should().Be(dateRaw);
    }
1
  • Try to use DateTimeOffset instead of DateTime. It should deserialize correctly. Commented Feb 2, 2016 at 17:56

1 Answer 1

6

I am not 100% sure what you are looking for here, but I think it is not safe to assume that JSON.Net will meet all of your needs without a little help. As Mr. Newton says:

Dates in JSON are hard.

The first thing is to determine whether or you want to support accepting unspecified dates or whether you are going to assume that all incoming dates are universal, even if they are missing the trailing Z.

If you assume that all incoming dates are universal, you can just see if they have a trailing Z and, if not, add it (not exactly production code, but you get the idea):

if (!dateString.EndsWith("Z\"", StringComparison.InvariantCultureIgnoreCase))
{
    dateString = dateString.Substring(0, dateString.LastIndexOf("\"", StringComparison.InvariantCultureIgnoreCase)) + "Z\"";
}

This change in assumption does require the dates you are testing for to be modified to be Utc.

If you do not want to assume that incoming dates are universal, but instead treat them as unspecified, you need to change the way that you are converting the incoming JSON by replacing:

JsonSerializerSettings settings = new JsonSerializerSettings();
settings.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
DateTime dateSerialized = JsonConvert.DeserializeObject<DateTime>(dateString, settings);                

with:

var oConverter = new Newtonsoft.Json.Converters.IsoDateTimeConverter();
DateTime dateSerialized = JsonConvert.DeserializeObject<DateTime>(dateString, oConverter);

This will result in an unspecified date that matches the dateString exactly. Here is where your helping hand comes into play:

if (dateSerialized.Kind == DateTimeKind.Unspecified)
{
    dateSerialized = dateSerialized.ToUniversalTime();
}

This means that the complete, revised first test will look like the following and it will pass:

    string dateString = "\"2014-06-02T21:00:00.0000000\"";
    DateTime dateRaw = new DateTime(2014, 6, 2, 21, 0, 0, 0, DateTimeKind.Unspecified);
    DateTime dateRawAsUtc = new DateTime(2014, 6, 3, 4, 0, 0, 0, DateTimeKind.Utc);
    dateRawAsUtc.Should().Be(dateRaw.ToUniversalTime());

    var oConverter = new Newtonsoft.Json.Converters.IsoDateTimeConverter();
    DateTime dateSerialized = JsonConvert.DeserializeObject<DateTime>(dateString, oConverter);
    if (dateSerialized.Kind == DateTimeKind.Unspecified)
    {
        dateSerialized = dateSerialized.ToUniversalTime();
    }

    Console.WriteLine("date string: " + dateString);
    Console.WriteLine("date serialized: " + dateSerialized.ToString("o"));

    dateSerialized.Kind.Should().Be(DateTimeKind.Utc); 
    dateSerialized.Should().Be(dateRaw.ToUniversalTime());
    dateSerialized.Should().Be(dateRawAsUtc);
Sign up to request clarification or add additional context in comments.

1 Comment

UGH!... but definitely thanks for the post/info... sounds like this is 'by design'... Now, as for the comment about supporting unspecified, this does often happen - e.g. a developer just myDate = new DateTime(2014, 10, 23); I'd like to have predictable behavior on what this means or happens, and if .Net does it one way, and JSON.Net does it another way, that's... ugly... and btw, I was using ServiceStack before, and they do it same as .Net.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.