12

In my ASP.NET Core Web-API project, I'm getting a HTTP POST call to one of my API controllers.

While evaluating the JSON payload and deserializing its contents, Json.NET stumbles upon a DateTime value of 0001-01-01T00:00:00 and can't convert it to a DateTimeOffset property.

I notice that the value propably should represent the value of DateTimeOffset.MinValue, but its lack of a timezone seems to trip the deserializer up. I can only imagine that the DateTimeOffset.Parse tries to translate it to the hosts current timezone, which results in an underflow of DateTimeOffset.MinValue.

The property is pretty simplistic:

[JsonProperty("revisedDate", NullValueHandling = NullValueHandling.Ignore)]
public DateTimeOffset? RevisedDate { get; set; }

And here is the response sent to the client:

{
    "resource.revisedDate": [
        "Could not convert string to DateTimeOffset: 0001-01-01T00:00:00. Path 'resource.revisedDate', line 20, position 44."
    ]
}

I'm using Newtonsoft.Json v11.0.2 and currently am in UTC + 2 (Germany). The exception traceback and error message are here: https://pastebin.com/gX9R9wq0.

I can't fix the calling code, so I have to fix it on my side of the line.

But the question is: How?

dbc
  • 104,963
  • 20
  • 228
  • 340
René Schindhelm
  • 1,887
  • 2
  • 14
  • 15
  • Can't reproduce, see https://dotnetfiddle.net/krfPNn. Also can't reproduce in UTC-05:00 using Json.NET version 10 or 11.2. What time zone are you in? What version of Json.NET are you using? – dbc May 31 '18 at 16:27
  • I'm using Newtonsoft.Json v11.0.2 and currently am in UTC + 2 (Germany). – René Schindhelm May 31 '18 at 16:31
  • Well if the problem is only reproducible in a time zone with a positive offset from UTC, that would be awkward. Can you reproduce this in a console app and share the complete `ToString()` output of the exception including the exception type, message, traceback and inner exception, if any? – dbc May 31 '18 at 16:33
  • Here's the exception https://pastebin.com/gX9R9wq0 – René Schindhelm May 31 '18 at 16:40
  • @dbc Here's the exception output from your demo https://pastebin.com/iAuXaJN6 – René Schindhelm May 31 '18 at 16:47
  • Oh that's interesting -- it fails on the initial call to `DateTimeOffset.Parse(s);` without ever getting to Json.NET. Can you share the traceback from this fiddle also? https://dotnetfiddle.net/xtnFJf – dbc May 31 '18 at 17:24
  • Here you go https://pastebin.com/dRSjNGKP – René Schindhelm May 31 '18 at 18:13
  • OK, after setting my machine's time zone to "(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna" I was able to reproduce the problem. Working on a workaround. – dbc May 31 '18 at 18:36

4 Answers4

18

The problem seems reproducible only when the machine's time zone TimeZoneInfo.Local has a positive offset from UTC, e.g. (UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna. I was unable to reproduce it in time zones with a non-positive offset such as UTC-05:00 or UTC itself.

Specifically, in JsonReader.ReadDateTimeOffsetString() a call is made to DateTimeOffset.TryParse using DateTimeStyles.RoundtripKind:

if (DateTimeOffset.TryParse(s, Culture, DateTimeStyles.RoundtripKind, out dt))
{
    SetToken(JsonToken.Date, dt, false);
    return dt;
}

This apparently causes an underflow error in time zones with a positive UTC offset. If in the debugger I parse using DateTimeStyles.AssumeUniversal instead, the problem is avoided.

You might want to report an issue about this to Newtonsoft. The fact that deserialization of a specific DateTimeOffset string fails only when the computer's time zone has certain values seems wrong.

The workaround is to use IsoDateTimeConverter to deserialize your DateTimeOffset properties with IsoDateTimeConverter.DateTimeStyles set to DateTimeStyles.AssumeUniversal. In addition it is necessary to disable the automatic DateTime recognition built into JsonReader by setting JsonReader.DateParseHandling = DateParseHandling.None, which must be done before the reader begins to parse the value for your DateTimeOffset properties.

First, define the following JsonConverter:

public class FixedIsoDateTimeOffsetConverter : IsoDateTimeConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(DateTimeOffset) || objectType == typeof(DateTimeOffset?);
    }

    public FixedIsoDateTimeOffsetConverter() : base() 
    {
        this.DateTimeStyles = DateTimeStyles.AssumeUniversal;
    }
}

Now, if you can modify the JsonSerializerSettings for your controller, use the following settings:

var settings = new JsonSerializerSettings
{
    DateParseHandling = DateParseHandling.None,
    Converters = { new FixedIsoDateTimeOffsetConverter() },
};

If you cannot easily modify your controller's JsonSerializerSettings you will need to grab DateParseHandlingConverter from this answer to How to prevent a single object property from being converted to a DateTime when it is a string and apply it as well as FixedIsoDateTimeOffsetConverter to your model as follows:

[JsonConverter(typeof(DateParseHandlingConverter), DateParseHandling.None)]
public class RootObject
{
    [JsonProperty("revisedDate", NullValueHandling = NullValueHandling.Ignore)]
    [JsonConverter(typeof(FixedIsoDateTimeOffsetConverter))]
    public DateTimeOffset? RevisedDate { get; set; }
}

DateParseHandlingConverter must be applied to the model itself rather than the RevisedDate property because the JsonReader will already have recognized 0001-01-01T00:00:00 as a DateTime before the call to FixedIsoDateTimeOffsetConverter.ReadJson() is made.

Update

In comments, @RenéSchindhelm writes, I created an issue to let Newtonsoft know. It is Deserialization of DateTimeOffset value fails depending on system's timezone #1731.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 2
    The workaround works. I [created an issue](https://github.com/JamesNK/Newtonsoft.Json/issues/1731) to let Newtonsoft know. Maybe they come up with a fix. I sincerely thank you dbc for providing this quality help here. +1 – René Schindhelm May 31 '18 at 19:42
  • 1
    I'm guessing that having a 31-12-9999 23:59:59 date with a negative time zone will also throw an exception on parsing – PLopes Jul 12 '19 at 12:42
8

This is what I am using to fix the issue in .NET Core 3.

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
            .AddNewtonsoftJson(options =>
            {
                options.SerializerSettings.MetadataPropertyHandling = MetadataPropertyHandling.Ignore;
                options.SerializerSettings.DateParseHandling = DateParseHandling.None;
                options.SerializerSettings.Converters.Add(new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal });
            });
...
Johann Blais
  • 9,389
  • 6
  • 45
  • 65
3

Change DateTimeOffset to DateTime solved the problem.

d219
  • 2,707
  • 5
  • 31
  • 36
faiz.sharp
  • 47
  • 2
0

Check your Json.NET version and then your input value and formatting. I'm trying the following example and it is working fine for me:

void Main()
{
    var json = @"{""offset"":""0001-01-01T00:00:00""}";
    var ds = Newtonsoft.Json.JsonConvert.DeserializeObject<TestDS>(json);
    Console.WriteLine(ds);
}
public class TestDS {
    [Newtonsoft.Json.JsonProperty("offset", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
    public DateTimeOffset? DSOffset { get; set; }
}

Here is the output:

DSOffset 1/1/0001 12:00:00 AM -06:00

  • 3
    The problem is reproducible in time zones with a positive offset from UTC, e.g. *`(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna`*. I was able to reproduce it after modifying my machine's time zone and re-running. – dbc May 31 '18 at 19:15