69

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";
    });
Dale K
  • 25,246
  • 15
  • 42
  • 71
D. English
  • 1,671
  • 1
  • 8
  • 7
  • 1
    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 offset – Panagiotis Kanavos Sep 25 '19 at 16:03
  • I'm asking how to globally set the date formatting in Startup.cs when using the new System.Text.Json – D. English Sep 25 '19 at 16:20
  • 2
    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](https://learn.microsoft.com/en-us/dotnet/api/system.datetime.kind?view=netframework-4.8). And I already explained that System.Text.Json *already* takes care of dates whose [DateTime.Kind](https://learn.microsoft.com/en-us/dotnet/api/system.datetime.kind?view=netframework-4.8) is UTC. Which means the dates you want to store are either Local or Unspecified. – Panagiotis Kanavos Sep 25 '19 at 16:23
  • 3
    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](https://learn.microsoft.com/en-us/dotnet/standard/datetime/system-text-json-support). 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 specified – Panagiotis Kanavos Sep 25 '19 at 16:25
  • 5
    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. – D. English Sep 25 '19 at 16:27
  • 1
    related issue here: https://github.com/dotnet/runtime/issues/1566 – mkb Jan 29 '21 at 16:33

5 Answers5

95

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());
     });
D. English
  • 1,671
  • 1
  • 8
  • 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. – Carlos Muñoz Oct 30 '20 at 07:07
  • 4
    the thing is question is about System.Text.Json not NewtonSoft.Json – mkb Jan 29 '21 at 17:39
  • 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` method – mkb Jan 29 '21 at 17:56
  • 1
    15 lines of code, a fragile modification in Startup, just to have a complete unambiguous date in my API output. This is credibility-affecting. – bbsimonbb Dec 14 '21 at 15:11
  • **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.) – bbsimonbb Dec 20 '21 at 10:27
  • 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 `null` – Nick Brooks Sep 15 '22 at 02:29
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...

Juan
  • 2,156
  • 18
  • 26
5

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);
    }
}
Dan Leonard
  • 99
  • 2
  • 2
0

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));
    }
}
Jonathan Allen
  • 68,373
  • 70
  • 259
  • 447
bbsimonbb
  • 27,056
  • 15
  • 80
  • 110
0

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

Rory
  • 40,559
  • 52
  • 175
  • 261