Json.NET deserializing DateTimeOffset value fails for DateTimeOffset.MinValue without timezone - c#

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?

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.

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 });
});
...

Change DateTimeOffset to DateTime solved the problem.

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

Related

Custom System.Text JsonConverter<DateTimeOffset> not getting invoked

I am creating a custom JsonConverterto parse datetimeoffset, to fix utc issue with offset. I am following MS doc
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SystemTextJsonSamples
{
public class DateTimeOffsetJsonConverter : JsonConverter<DateTimeOffset>
{
public override DateTimeOffset Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options) =>
DateTimeOffset.ParseExact(reader.GetString()!,
"MM/dd/yyyy", CultureInfo.InvariantCulture);
public override void Write(
Utf8JsonWriter writer,
DateTimeOffset dateTimeValue,
JsonSerializerOptions options) =>
writer.WriteStringValue(dateTimeValue.ToString(
"MM/dd/yyyy", CultureInfo.InvariantCulture));
}
}
I have registered the converter in the startup like so
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.JsonSerializerOptions.Converters.Add(new DateTimeConverter());
options.JsonSerializerOptions.Converters.Add(new DateTimeOffsetConverter());
})
and here is my model
[Serializable()]
public class Travel
{
public DateTimeOffset TravelTime { get; set; }
}
When i make call to my api, my custom converter for datetimeoffset is not getting called. Please note that i also have a customdate converter which is working as expected.
Why is my offsetdatetime converter not getting invoked when i serialize/deserialize.
I am using .Net core 6
It's not enough to define a JsonConverter, you also have to apply it to the property, like this:
[JsonConverter(typeof(DateTimeOffsetJsonConverter))]
public DateTimeOffset TravelTime { get; set; }
Then it will be used for serialization and deserialization.
The reason for this (like why can't it pick up the type?) is that you can have several converters defined for the same type and apply them to the properties that need them. Now you can actually make different 'string to string' converters (for example).
Firstly, it should be DateTimeOffsetJsonConverter instead of DateTimeOffsetConverter, change your code to:
builder.Services.AddControllersWithViews().AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.JsonSerializerOptions.Converters.Add(new DateTimeConverter());
//options.JsonSerializerOptions.Converters.Add(new DateTimeOffsetConverter());
options.JsonSerializerOptions.Converters.Add(new DateTimeOffsetJsonConverter());
});
Then, be sure you post the data with content type application/json. For example:
Besides, you can also add [FromBody] to specify the source because it binds the form data by default if you use asp.net core MVC project:
[HttpPost]
public IActionResult Index([FromBody]Travel model)
{
//do your stuff...
}

C# code first grpc : custom protobuf converter for DateTime

I'm using something like this in dotnet asp net core 6:
<PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.0.152" />
<PackageReference Include="protobuf-net.Grpc.AspNetCore.Reflection" Version="1.0.152" />
[DataContract]
public class TaskItem
{
//other properties omitted
[DataMember(Order = 5)]
public DateTime DueDate { get; set; } = null!;
}
Now, when I call the service with grpcurl
"DueDate": {
"value": "458398",
"scale": "HOURS"
}
And in the generated proto file
import "protobuf-net/bcl.proto"; // schema for protobuf-net's handling of core .NET types
message TaskItem {
//other properties omitted
.bcl.DateTime DueDate = 5;
Is there a way to specify a custom converter so that it will serialize to ISO 8601 string in order to better support cross platform (I'll have some clients in js where having a string is ok since I just need new Date(v) and d.toISOString()) ?
I know I can just declare DueDate as string, but then the "problem" is that when I use C# code-first client I also need to convert back to DateTime and to string ...
For example, I can do the following with JSON
.AddJsonOptions(x =>
{
x.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
What you ask is very different from a JSON type converter. As the docs explain the standard way of serializing dates is the google.protobuf.Timestamp type. That's defined in the proto file. When you use code-first that file is generated by the open source protobuf-net.Grpc tool.
To use the Timestamp type you need to tell the tool to format that property using a well-known type with the ProtoMember attribute :
[ProtoMember(1, DataFormat = DataFormat.WellKnown)]
public DateTime Time { get; set; }
This is shown in the tool's Getting Started document.
This isn't the default for legacy reasons :
(for legacy reasons, protobuf-net defaults to a different library-specific layout that pre-dates the introduction of .google.protobuf.Timestamp). It is recommended to use DataFormat.WellKnown on DateTime and TimeSpan values whenever possible.

ASP.NET MVC How to handle dynamic datetime format from database

In my ASP.NET MVC 5 dwith EF 6 project, I have a database where datetime format is stored as string like "dd-MM-yyyy". User can change this format any time. User will use the given format in the date fields in the view. But when they will post that. Automatically it will bind as a DateTime for that property. I am statically handling it by the following code
[DataType(DataType.Time), DisplayFormat(DataFormatString = "{HH:mm}", ApplyFormatInEditMode = true)]
public DateTime? EndingTime { get; set; }
public string EndingTimeValue
{
get
{
return EndingTime.HasValue ? EndingTime.Value.ToString("HH:mm") : string.Empty;
}
set
{
EndingTime = DateTime.Parse(value);
}
}
but I know it's not a best way to do that. There may need a model binder or filter or any kind of custom attribute. I will be greatly helped if you give me a efficient solution with sample code. Thanks in advance.
NB: I am using razor view engine. and my solution consists of 7 projects. So there is no chance of using Session in model. Again I have a base repository class for using entity framework.
People usually store the datetime in the database as a datetime.
Then wherever you do a translation from datetime to string that datetime can be displayed in a format that depends on the culture of the viewer.
By doing this you can quickly make a page with datetime formats that will format the datetimes nicely wherever you are.
change the culture you pass to the toString and the format changes.
please see this MSDN page for more info about it.
edit: (see comments below)
anywhere on server:
string WhatYouWant = yourTime.ToCustomFormat()
and create an extension method for the datetime that gets the format out of the database and returns a string in the correct format.
public static class MyExtensions
{
public static string ToCustomFormat(this DateTime yourTime)
{
// Get the following var out of the database
String format = "MM/dd/yyyy hh:mm:sszzz";
// Converts the local DateTime to a string
// using the custom format string and display.
String result = yourTime.ToString(format);
return result;
}
}
This will allow you to call it anywhere anytime on your server. You can't access the method client side in javascript. I hope this helps.
(To be honest I'm a new developer too and still have a lot to learn ^^)
I have tried many options regarding this problem. Now what I am doing is created an action filter to catch all the DateTime and nullable DateTime Fields. Here I am providing the binder.
public class DateTimeBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
DateTime date;
var displayFormat = SmartSession.DateTimeFormat;
if (DateTime.TryParseExact(value.AttemptedValue, displayFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out date))
{
return date;
}
else
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName,"Invalid Format");
}
return base.BindModel(controllerContext, bindingContext);
}
}
in views the code I am formatting the date using same date format.

Serialize/Deserialize Json DateTime in Neo4jClient

I use Neo4jClient to use Neo4j, I use cypher code for CRUD entity , Follow code :
_graphClient.Cypher.Merge("(n:Movie { Id:101 })")
.Set("n.Key = 55,n.DateTime='" +DateTime.UtcNow.ToString()+"'").ExecuteWithoutResults();
_graphClient.Cypher
.Match("(n:Movie)-[r:RelName]-(m:Movie)")
.Where((EntityNode n) => n.Id == 20)
.Return.......
public class EntityNode
{
public int Id { get; set; }
public string Key { get; set; }
public DateTime DateTime { get; set; }
}
ERROR :Neo4j returned a valid response, however Neo4jClient was unable to deserialize into the object structure you supplied.Can't deserialize DateTime.
On other hand i use jsonconvertor in different ways, for example :
_graphClient.Cypher.Merge("(n:Movie { Id:101 })")
.Set("n.Key = 55,n.DateTime=" +JsonConvert.SerializeObject(DateTime.UtcNow)).ExecuteWithoutResults();
I still have the ERROR
Pass it as a proper parameter:
graphClient.Cypher
.Merge("(n:Movie { Id:101 })")
.Set("n.Key = {key}, n.DateTime = {time}")
.WithParams(new {
key = 55,
time = DateTimeOffset.UtcNow
})
.ExecuteWithoutResults();
This way, Neo4jClient will do the serialization for you, and you don't introduce lots of security and performance issues.
This is in the doco here: https://github.com/Readify/Neo4jClient/wiki/cypher#parameters
I have faced the same issue recently its because of date time value coming from neo.
I have stored the date time in neo as epoch time but while retrieving i used long in the class. because of this its given me the above error.
Try using string for the above date time.
Hope this helps you.

MVC 1 Parameter Binding

I'm passing a date to my server in Invariant Culture, the following format
'mm/dd/yy'
The parameter binding in MVC fails to parse this date and returns null for the parameter. This is persumably because IIS is running on a machine using English culture ('dd/mm/yy' works just fine).
I want to override the parsing of all date on my server to use Invariant Culture like so...
Convert.ChangeType('12/31/11', typeof(DateTime), CultureInfo.InvariantCulture);
even when the date is part of another object...
public class MyObj
{
public DateTime Date { get; set; }
}
My controller method is something like this....
public ActionResult DoSomethingImportant(MyObj obj)
{
// use the really important date here
DoSomethingWithTheDate(obj.Date);
}
The date is being sent as Json data like so....
myobj.Date = '12/31/11'
I've tried adding an implementation of IModelBinder to the binderDictionary in the global.asax
binderDictionary.Add(typeof(DateTime), new DateTimeModelBinder());
That doesn't work, and neither does
ModelBinders.Binders.Add(typeof(DateTime), new DataTimeModelBinder());
This seems like some ppl would want to do all the time. I can't see why you would parse dates etc. in the current culture on the server. A client would have to find out the culture of the server just to format dates the server will be able to parse.....
Any help appreciated!
I've solved the problem here, what I had missed was that in the object, the datetime was nullable
public class MyObj
{
public DateTime? Date { get; set; }
}
Hence my binder wasn't being picked up.
If anyone is interested, this is what I did....
In the global.asax added the following
binderDictionary.add(typeof(DateTime?), new InvariantBinder<DateTime>());
Created an invariant binder like so
public class InvariantBinder<T> : IModelBinder
{
public object BindModel(ControllerContext context, ModelBindingContext binding)
{
string name = binding.ModelName;
IDictionary<string, ValueProviderResult> values = binding.ValueProvider;
if (!values.ContainsKey(name) || string.IsNullOrEmpty(values[names].AttemptedValue)
return null;
return (T)Convert.ChangeType(values[name].AttemptedValue, typeof(T), CultureInfo.Invariant);
}
}
Hope this comes in handy for someone else.....
Is your problem that your custom model binder is unable to parse some of the input dates or that your custom model binder never gets called? If it's the former then trying to use the culture of the user's browser might help.
public class UserCultureDateTimeModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object value = controllerContext.HttpContext.Request[bindingContext.ModelName];
if (value == null)
return null;
// Request.UserLanguages could have multiple values or even no value.
string culture = controllerContext.HttpContext.Request.UserLanguages.FirstOrDefault();
return Convert.ChangeType(value, typeof(DateTime), CultureInfo.GetCultureInfo(culture));
}
}
...
ModelBinders.Binders.Add(typeof(DateTime?), new UserCultureDateTimeModelBinder());
Is it possible to pass the date to the server in ISO 8601 format? I think the server would parse that correctly regardless of its own regional settings.

Categories