Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does JsonConvert change time of DateTimes with DateTimeKind.Unspecified when using DateTimeStyles.AssumeUniversal?

I'm building a web API and am having trouble with the JSON serialization of DateTimes. After doing some tests I can only conclude that the behavior of Newtonsoft.Json.JsonConvert and/or the Newtonsoft IsoDateTimeConverter is not what I expected.

Consider this:

// Arrange
var noonUtc = new DateTime(2016, 05, 12, 12, 0, 0, DateTimeKind.Utc);
var noon = new DateTime(2016, 05, 12, 12, 0, 0, DateTimeKind.Unspecified);

var settings = new JsonSerializerSettings();

settings.Converters.Add(new IsoDateTimeConverter
{    
    Culture = CultureInfo.InvariantCulture,    
    DateTimeStyles = DateTimeStyles.AdjustToUniversal
});

// Act
var utcJson = JsonConvert.SerializeObject(noonUtc, settings); // "\"2016-05-12T12:00:00Z\""
var json = JsonConvert.SerializeObject(noon, settings);       // "\"2016-05-12T10:00:00Z\""

... // Assertions

Okay, so the time for the DateTime with DateTimeKind.Unspecified has been adjusted from 12 o'clock to 10 o'clock. I'm in Stockholm which is currently two hours ahead of UTC, so fair enough.

However, let's change the serializer settings to use DateTimeStyles.AssumeUniversal, like so:

settings.Converters.Add(new IsoDateTimeConverter
{    
    Culture = CultureInfo.InvariantCulture,    
    DateTimeStyles = DateTimeStyles.AssumeUniversal
});

This results in the exact same strings and thus also adjusts the DateTime with DateTimeKind.Unspecified by two hours! Should it not assume the date time was already UTC time and leave the time as it was? What am I missing here?

like image 432
Lurifaxel Avatar asked May 13 '16 06:05

Lurifaxel


1 Answers

I don't think you're missing anything; this looks like it might be a bug in the IsoDateTimeConverter. Here is the relevant code from the source:

if ((_dateTimeStyles & DateTimeStyles.AdjustToUniversal) == DateTimeStyles.AdjustToUniversal
   || (_dateTimeStyles & DateTimeStyles.AssumeUniversal) == DateTimeStyles.AssumeUniversal)
{
    dateTime = dateTime.ToUniversalTime();
}

As you can see, it only looks at whether _dateTimeStyles is set to AdjustToUniversal or AssumeUniversal before calling ToUniversalTime(); it never checks the date's Kind property.

And the documentation for DateTime.ToUniversalTime() says this:

Starting with the .NET Framework version 2.0, the value returned by the ToUniversalTime method is determined by the Kind property of the current DateTime object. The following table describes the possible results.

Kind        | Results
----------- | ----------------------------------------------------------
Utc         | No conversion is performed.
Local       | The current DateTime object is converted to UTC.
Unspecified | The current DateTime object is assumed to be a local time, 
            | and the conversion is performed as if Kind were Local.

So yeah, it looks like the converter should definitely not be calling ToUniversalTime in this situation. You might want to report an issue.

For now, you can work around this issue by implementing a replacement converter (derived from the original) with the correct behavior. This is probably closer to what you would want:

public class CorrectedIsoDateTimeConverter : IsoDateTimeConverter
{
    private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK";

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value is DateTime)
        {
            DateTime dateTime = (DateTime)value;

            if (dateTime.Kind == DateTimeKind.Unspecified)
            {
                if (DateTimeStyles.HasFlag(DateTimeStyles.AssumeUniversal))
                {
                    dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
                }
                else if (DateTimeStyles.HasFlag(DateTimeStyles.AssumeLocal))
                {
                    dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Local);
                }
            }

            if (DateTimeStyles.HasFlag(DateTimeStyles.AdjustToUniversal))
            {
                dateTime = dateTime.ToUniversalTime();
            }

            string format = string.IsNullOrEmpty(DateTimeFormat) ? DefaultDateTimeFormat : DateTimeFormat;
            writer.WriteValue(dateTime.ToString(format, Culture));
        }
        else
        {
            base.WriteJson(writer, value, serializer);
        }
    }
}
like image 61
Brian Rogers Avatar answered Oct 16 '22 13:10

Brian Rogers