Formatting DateTime in ASP.NET Core 3.0 using System.Text.Json
Asked Answered
E

6

73

I am migrating a web API from .NET Core 2.2 to 3.0 and want to use the new System.Text.Json. When using Newtonsoft I was able to format DateTime using the code below. How can I accomplish the same?

.AddJsonOptions(options =>
    {
        options.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc;
        options.SerializerSettings.DateFormatString = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ";
    });
Epideictic answered 25/9, 2019 at 15:47 Comment(6)
What's the actual question? How to treat DateTime as UTC even if it's not? Both JSON.NET and System.Text.Json use ISO8601 by default. If the DateTimeKind is UTC, Z is appended to the string. A local time will include the local timezone offsetSupplejack
I'm asking how to globally set the date formatting in Startup.cs when using the new System.Text.JsonEpideictic
That's not what your code does though, since JSON.NET already uses ISO8601- the same format you used. What you did there was force it to use UTC for all DateTime kinds. And I already explained that System.Text.Json already takes care of dates whose DateTime.Kind is UTC. Which means the dates you want to store are either Local or Unspecified.Supplejack
Why do you want to convert to UTC though? Why not let System.Text.Json emit the offset? In any case, date formatting is explained in DateTime and DateTimeOffset support in System.Text.Json. There's no way to force the format short of creating a custom formatter. You could make sure all the dates you use are UTC or use DateTimeOffset to make sure the offset is specifiedSupplejack
I want to serialize the DateTime without the fractional seconds, and always UTC. When accessing my API using swift (iOS app) the fractional seconds and offset causes a json parsing failure.Epideictic
related issue here: github.com/dotnet/runtime/issues/1566Bower
E
101

Solved with a custom formatter. Thank you Panagiotis for the suggestion.

public class DateTimeConverter : JsonConverter<DateTime>
{
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        Debug.Assert(typeToConvert == typeof(DateTime));
        return DateTime.Parse(reader.GetString());
    }

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"));
    }
}


// in the ConfigureServices()
services.AddControllers()
    .AddJsonOptions(options =>
     {
         options.JsonSerializerOptions.Converters.Add(new DateTimeConverter());
     });
Epideictic answered 25/9, 2019 at 16:53 Comment(7)
There are two problems with this code. 1) NewtonSoft.Json doesn't always call .ToUniversalTime() on value. It depends on its DateTimeKind. The provided format string strips down the 7 decimal precision that NewtonSoft.Json mantains. I can update your answer with the correct code if you are ok with it. Otherwise i can create a new answer with the correct code.Brynne
the thing is question is about System.Text.Json not NewtonSoft.JsonBower
Since I'am already using UTC in client code too I exluded ToUniversalTime(), well all I needed was the Z at the end of the date string and this accoplished that, this is the correct approach I guess?!. also apparantly reader has a method reader.GetDateTime() which can be used in Read methodBower
15 lines of code, a fragile modification in Startup, just to have a complete unambiguous date in my API output. This is credibility-affecting.Uterine
Beware the timezone! The above code will assume your C# DateTime is in local time, and will convert it to UTC before serializing as UTC. If, to stay sane, all your dates are already UTC, an unwanted timezone delta will sneak in at this step. (To fix this, just delete ToUniversalTime() in the Write() method.)Uterine
Suggestion for Read method - Check for when reader.GetString() is null (it returns nullable string) and if it is null return JsonException. e.g. if (value is null) throw new JsonException(); as that is what the default JsonSerializer does for a DateTime type when the JSON value is nullSummation
You need to pass CultureInfo.InvariantCulture to DateTime.Parse() and DateTime.ToString(). If you don't, and the current culture uses a calendar other than GregorianCalendar, you may get unexpected or wrong results because the year and month start from a different year 1. For instance, in the "ar-SA" culture, if I serialize the date 01/01/2000 with this converter I get "1420-09-24T00:00:00Z", see dotnetfiddle.net/YJeJWj.Pearlpearla
P
19

Migrating to Core 3 I had to replace System.Text.Json to use Newtonsoft again by :

services.AddControllers().AddNewtonsoftJson();

But I was having same issue with UTC dates in an Angular app and I had to add this to get dates in UTC:

services.AddControllers().AddNewtonsoftJson(
       options => options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc);

In your case you should be able to do this:

services.AddControllers().AddNewtonsoftJson(options =>
    {
        options.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc;
        options.SerializerSettings.DateFormatString = "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ";
    });

It works and I hope it helps...

Peta answered 21/3, 2020 at 6:19 Comment(1)
question was not about newtonsoft.jsonPepsinogen
D
7

This is more or less the same as others have suggested, but with an additional step to take the format string as a parameter in the attribute.

The formatter:

public class DateTimeFormatConverter : JsonConverter<DateTime>
{
    private readonly string format;

    public DateTimeFormatConverter(string format)
    {
        this.format = format;
    }

    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return DateTime.ParseExact(
            reader.GetString(),
            this.format,
            CultureInfo.InvariantCulture);
    }

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        ArgumentNullException.ThrowIfNull(writer, nameof(writer));

        writer.WriteStringValue(value
            .ToUniversalTime()
            .ToString(
                this.format,
                CultureInfo.InvariantCulture));
    }
}

Since JsonConverterAttribute is not sealed, we can do something like this:

public sealed class JsonDateTimeFormatAttribute : JsonConverterAttribute
{
    private readonly string format;

    public JsonDateTimeFormatAttribute(string format)
    {
        this.format = format;
    }

    public string Format => this.format;

    public override JsonConverter? CreateConverter(Type typeToConvert)
    {
        return new DateTimeFormatConverter(this.format);
    }
}
Durrace answered 18/4, 2022 at 14:57 Comment(0)
U
2

This dumpster fire of asp.net core date serialization/deserialization is maybe easier to understand when you see the dumpster fire of Date.Parse() and Date.ParseExact(). We're passing dates to and from javascript, so we don't want to be formatting. We just want to transparently serialize and deserialize between DateTime and ISO 8601 in UTC.

That this is not the default, and that there's no easy configuration option, and that the solution is so funky and fragile, is credibility-destroying. This is currently what's working for me, based on D.English's answer for writing, and the linked answer for reading, and using this answer to access the JsonDocument correctly...

Update this is for the dumptser fire of model binding. For the dumpster fire of query string parsing, it's over here

// in Startup.cs ConfigureServices()

services.AddMvc().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.Converters.Add(new UtcDateTimeConverter());
});


public class BobbyUtcDateTimeConverter : JsonConverter<DateTime>
{
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        using (var jsonDoc = JsonDocument.ParseValue(ref reader))
        {
            var stringValue = jsonDoc.RootElement.GetRawText().Trim('"').Trim('\'');
            var value = DateTime.Parse(stringValue, null, System.Globalization.DateTimeStyles.RoundtripKind);
            return value;
        }
    }

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", System.Globalization.CultureInfo.InvariantCulture));
    }
}
Uterine answered 21/12, 2021 at 13:46 Comment(1)
DateTime.SpecifyKind doesn't work. If you round-trip a date, it will be off by your local time zone.Ambassadoratlarge
T
1

Nowadays in asp.net 6/7 if you set DateTime.Kind to Utc then your DateTime objects will be serialized with a trailing 'Z' to indicate UTC timezone.

If your data comes from efcore you can tell it to treat all DateTime data as UTC, e.g. like this

Taradiddle answered 26/11, 2022 at 20:47 Comment(0)
S
0

STJ doesn't handle nulls, empty strings, or anything really bizarre correctly. That means there are a ton of gotchas in every single STJ implementation. My options looks like this:

    public static JsonSerializerOptions AddOptions(this JsonSerializerOptions options)
{
    if (options == null)
    {
        options = new JsonSerializerOptions();
    }

    options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
    options.PropertyNameCaseInsensitive = true;
    options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    options.NumberHandling = JsonNumberHandling.AllowReadingFromString;

    options.Converters.Add(new DateTimeConverter());
    options.Converters.Add(new NullableDateTimeConverter());
    options.Converters.Add(new NullableIntConverter());
    options.Converters.Add(new NullableLongConverter());
    options.Converters.Add(new NullableDecimalConverter());
    options.Converters.Add(new BooleanConverter());
    options.Converters.Add(new JsonStringEnumConverter());

    return options;
}

It would be used like this:

services.AddMvc(options => options.EnableEndpointRouting = false).AddJsonOptions(o => JsonExtensions.AddOptions(o.JsonSerializerOptions));

This is a preface to WHY the accepted answer may not be wholly correct. First, deserialization from null and empty strings simply won't work. Second, deserialization from slashes in the date simply won't work. Third, deserialization from a string with Z at the end may throw errors in times: var myDate = "2023-12-01T01:00:00Z"; with a simple DateTime.Parse may result in a 2023-11-30 timeframe because timezones. Therefore, I recommend trying the default UTFJsonReader, if it doesn't work, then in the catch use DateTimeOffset. The DateTimeOffset will not allow you to specify the Kind on it. So, create a new DateTime, specifying that it is UTC instead of unspecified and then you can convert back and forth without further issue.

public class DateTimeConverter : JsonConverter<DateTime>
    {
        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            try
            {
                return reader.GetDateTime();
            }
            catch
            {
                if (reader.TokenType == JsonTokenType.String)
                {

                    var info = reader.GetString();
                    var offset = DateTimeOffset.Parse(reader.GetString() ?? string.Empty);

                    if (info?.ToLower()?.EndsWith('z') == true)
                    {
                        return new DateTime(offset.DateTime.Ticks, DateTimeKind.Utc);
                    }

                    return offset.DateTime;
                }

                throw;
            }
        }

        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value);
        }
    }

I realize my above information does not answer wholly your question... but it does show you where you can do these kinds of things and just how undeveloped but customizable STJ is. For example for your shenanigans:

writer.WriteStringValue(value.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"));  //why do you hate me?
Sherleysherline answered 30/1 at 1:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.