Why does ServiceStack emit local time even if date was UTC in JSON?
Asked Answered
G

1

11

Long story short- a date round tripped through ServiceStack.Text's JSON parser loses time zone information. Oddly enough, DateTimeSerializerTests.DateTime_Is_Serialized_As_Utc_and_Deserialized_as_local() seems to expect this behavior, and DateTimeSerializer.Prepare() explicitly calls ToLocalTime() on every date time object that was parsed as UTC!

Here is an example test case (MSTest, but easy enough to run in anything). Local passes, but UTC and Unspecified do not - the kind returned by the DateTime object is always 'Local'.

[TestMethod]
public void TestParseSingleDateTime_UTC()
{
    // In canonical UTC format
    var date = "2014-06-03T14:26:20.0030000Z";
    var raw = new DateTime(2014, 6, 3, 14, 26, 20, 3, DateTimeKind.Utc);
    var value = DateTimeSerializer.ParseShortestXsdDateTime(date);
    Assert.AreEqual(DateTimeKind.Utc, value.Kind);
    Assert.AreEqual(raw, value);
}

[TestMethod]
public void TestParseSingleDateTime_Local()
{
    // In local time zone
    var date = "2014-06-02T11:15:49.1480000-05:00";
    var raw = new DateTime(2014, 6, 2, 11, 15, 49, 148, DateTimeKind.Local);
    var value = DateTimeSerializer.ParseShortestXsdDateTime(date);
    Assert.AreEqual(DateTimeKind.Local, value.Kind);
    Assert.AreEqual(raw, value);
}

[TestMethod]
public void TestParseSingleDateTime_Unspecified()
{
    // Unspecified time zone, as we would parse from Excel cells with dates
    var date = "2012-01-06T00:00:00.0000000";
    var raw = new DateTime(2012, 1, 6, 0, 0, 0, DateTimeKind.Unspecified);
    var value = DateTimeSerializer.ParseShortestXsdDateTime(date);
    Assert.AreEqual(DateTimeKind.Unspecified, value.Kind);
    Assert.AreEqual(raw, value);
}

Why on earth is this default behavior? Using JsConfig.AlwaysUseUtc isn't a good workaround here, because then I can't parse a local timestamp as local either.

Grindelia answered 3/6, 2014 at 17:19 Comment(2)
In my application always using utc was fine. But if that doesn't work for you, perhaps using DateTimeOffset instead of DateTime gives you the necessary control.Teletypewriter
This is actually due to the way DateTime works in Microsoft. I wrote a patch to correct it in Service Stack which was never merged for fear of breaking backwards compatibility or something. I can dig back through and see if I can find it.Chipmunk
M
2

If anyone finds this, although it is old, this logic should be able to be fully controlled through the JSON parser's configuration, available globally as JsConfig.

The below example (although untested) should roughly cover the scenario as I understand it above:

// Formats to use for the different date kinds
string utcTimeFormat = "yyyy-MM-dd'T'HH:mm:ss.fffffff'Z'";
string localTimeFormat = "yyyy-MM-dd'T'HH:mm:ss.fffffff";

// Serialization function
// Check if specified as UTC otherwise treat as local. 
JsConfig<DateTime>.SerializeFn = datetime =>
{
    switch (datetime.Kind)
    {
        case DateTimeKind.Utc:
            return datetime.ToString(utcTimeFormat);
        default: //DateTimeKind.Unspecified and DateTimeKind.Local
            return datetime.ToString(localTimeFormat);
    }
};

// Deserialization function
// Check which format provided, attempt to parse as datetime or return minValue.
JsConfig<DateTime>.DeSerializeFn = datetimeStr =>
{
    if (string.IsNullOrWhiteSpace(datetimeStr))
    {
        return DateTime.MinValue;
    }

    if (datetimeStr.EndsWith("Z") && 
        DateTime.TryParseExact(datetimeStr, utcTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTime resultUtc))
    {
        return resultUtc;
    }
    else if (!datetimeStr.EndsWith("Z") && 
        DateTime.TryParseExact(datetimeStr, localTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out DateTime resultLocal))
    {
        return resultLocal;
    }

    return DateTime.MinValue;
};

Why it happens would either be a design choice or oversight which I cannot comment on.

Museology answered 31/1, 2020 at 2:53 Comment(1)
See my comment above. OP is correct. However, this is a Microsoft issue, not necessarily solely due to SeviceStack. However, I did write a patch for it and thus used my own branch of the service stack library.Chipmunk

© 2022 - 2024 — McMap. All rights reserved.