Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 858
Dan McGee Avatar asked Jun 03 '14 17:06

Dan McGee


1 Answers

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.

like image 55
Jacob Smit Avatar answered Oct 25 '22 02:10

Jacob Smit