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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With