Unterstützung von DateTime und DateTimeOffset in System.Text.Json

Die Bibliothek System.Text.Json analysiert und schreibt DateTime- und DateTimeOffset-Werte gemäß dem erweiterten Profil „ISO 8601-1:2019“. Konverter bieten benutzerdefinierte Unterstützung für die Serialisierung und Deserialisierung mit JsonSerializer. Sie können auch Utf8JsonReader und Utf8JsonWriter verwenden, um benutzerdefinierte Unterstützung zu implementieren.

Unterstützung des ISO 8601-1:2019-Formats

Die Typen JsonSerializer, Utf8JsonReader, Utf8JsonWriter und JsonElement analysieren und schreiben DateTime- und DateTimeOffset-Textdarstellungen gemäß dem erweiterten Profil des ISO 8601-1:2019-Formats. Beispiel: 2019-07-26T16:59:57-05:00.

DateTime- und DateTimeOffset-Daten können mit JsonSerializer serialisiert werden:

using System.Text.Json;

public class Example
{
    private class Product
    {
        public string? Name { get; set; }
        public DateTime ExpiryDate { get; set; }
    }

    public static void Main(string[] args)
    {
        Product p = new Product();
        p.Name = "Banana";
        p.ExpiryDate = new DateTime(2019, 7, 26);

        string json = JsonSerializer.Serialize(p);
        Console.WriteLine(json);
    }
}

// The example displays the following output:
// {"Name":"Banana","ExpiryDate":"2019-07-26T00:00:00"}

Zum Deserialisieren von DateTime und DateTimeOffset kann ebenfalls JsonSerializer verwendet werden:

using System.Text.Json;

public class Example
{
    private class Product
    {
        public string? Name { get; set; }
        public DateTime ExpiryDate { get; set; }
    }

    public static void Main(string[] args)
    {
        string json = @"{""Name"":""Banana"",""ExpiryDate"":""2019-07-26T00:00:00""}";
        Product p = JsonSerializer.Deserialize<Product>(json)!;
        Console.WriteLine(p.Name);
        Console.WriteLine(p.ExpiryDate);
    }
}

// The example displays output similar to the following:
// Banana
// 7/26/2019 12:00:00 AM

Bei Verwendung der Standardoptionen müssen eingegebene DateTime- und DateTimeOffset-Textdarstellungen dem erweiterten ISO 8601-1:2019-Profil entsprechen. Wenn Sie versuchen, Darstellungen zu deserialisieren, die nicht dem Profil entsprechen, wird von JsonSerializer eine JSON-Ausnahme (JsonException) ausgelöst:

using System.Text.Json;

public class Example
{
    private class Product
    {
        public string? Name { get; set; }
        public DateTime ExpiryDate { get; set; }
    }

    public static void Main(string[] args)
    {
        string json = @"{""Name"":""Banana"",""ExpiryDate"":""26/07/2019""}";
        try
        {
            Product _ = JsonSerializer.Deserialize<Product>(json)!;
        }
        catch (JsonException e)
        {
            Console.WriteLine(e.Message);
        }
    }
}

// The example displays the following output:
// The JSON value could not be converted to System.DateTime. Path: $.ExpiryDate | LineNumber: 0 | BytePositionInLine: 42.

Das JSON-Dokument (JsonDocument) bietet strukturierten Zugriff auf den Inhalt von JSON-Nutzdaten – einschließlich DateTime- und DateTimeOffset-Darstellungen. Im folgenden Beispiel wird gezeigt, wie auf der Grundlage einer Sammlung von Temperaturen die durchschnittliche Temperatur an Montagen berechnet wird:

using System.Text.Json;

public class Example
{
    private static double ComputeAverageTemperatures(string json)
    {
        JsonDocumentOptions options = new JsonDocumentOptions
        {
            AllowTrailingCommas = true
        };

        using (JsonDocument document = JsonDocument.Parse(json, options))
        {
            int sumOfAllTemperatures = 0;
            int count = 0;

            foreach (JsonElement element in document.RootElement.EnumerateArray())
            {
                DateTimeOffset date = element.GetProperty("date").GetDateTimeOffset();

                if (date.DayOfWeek == DayOfWeek.Monday)
                {
                    int temp = element.GetProperty("temp").GetInt32();
                    sumOfAllTemperatures += temp;
                    count++;
                }
            }

            double averageTemp = (double)sumOfAllTemperatures / count;
            return averageTemp;
        }
    }

    public static void Main(string[] args)
    {
        string json =
                @"[" +
                    @"{" +
                        @"""date"": ""2013-01-07T00:00:00Z""," +
                        @"""temp"": 23," +
                    @"}," +
                    @"{" +
                        @"""date"": ""2013-01-08T00:00:00Z""," +
                        @"""temp"": 28," +
                    @"}," +
                    @"{" +
                        @"""date"": ""2013-01-14T00:00:00Z""," +
                        @"""temp"": 8," +
                    @"}," +
                @"]";

        Console.WriteLine(ComputeAverageTemperatures(json));
    }
}

// The example displays the following output:
// 15.5

Wenn Sie versuchen, die durchschnittliche Temperatur anhand von Nutzdaten mit nicht konformen DateTime-Darstellungen zu berechnen, wird von JsonDocument eine Formatausnahme (FormatException) ausgelöst:

using System.Text.Json;

public class Example
{
    private static double ComputeAverageTemperatures(string json)
    {
        JsonDocumentOptions options = new JsonDocumentOptions
        {
            AllowTrailingCommas = true
        };

        using (JsonDocument document = JsonDocument.Parse(json, options))
        {
            int sumOfAllTemperatures = 0;
            int count = 0;

            foreach (JsonElement element in document.RootElement.EnumerateArray())
            {
                DateTimeOffset date = element.GetProperty("date").GetDateTimeOffset();

                if (date.DayOfWeek == DayOfWeek.Monday)
                {
                    int temp = element.GetProperty("temp").GetInt32();
                    sumOfAllTemperatures += temp;
                    count++;
                }
            }

            double averageTemp = (double)sumOfAllTemperatures / count;
            return averageTemp;
        }
    }

    public static void Main(string[] args)
    {
        // Computing the average temperatures will fail because the DateTimeOffset
        // values in the payload do not conform to the extended ISO 8601-1:2019 profile.
        string json =
                @"[" +
                    @"{" +
                        @"""date"": ""2013/01/07 00:00:00Z""," +
                        @"""temp"": 23," +
                    @"}," +
                    @"{" +
                        @"""date"": ""2013/01/08 00:00:00Z""," +
                        @"""temp"": 28," +
                    @"}," +
                    @"{" +
                        @"""date"": ""2013/01/14 00:00:00Z""," +
                        @"""temp"": 8," +
                    @"}," +
                @"]";

        Console.WriteLine(ComputeAverageTemperatures(json));
    }
}

// The example displays the following output:
// Unhandled exception.System.FormatException: One of the identified items was in an invalid format.
//    at System.Text.Json.JsonElement.GetDateTimeOffset()

Der untergeordnete UTF-8-JSON-Writer (Utf8JsonWriter) schreibt DateTime- und DateTimeOffset-Daten:

using System.Text;
using System.Text.Json;

public class Example
{
    public static void Main(string[] args)
    {
        JsonWriterOptions options = new JsonWriterOptions
        {
            Indented = true
        };

        using (MemoryStream stream = new MemoryStream())
        {
            using (Utf8JsonWriter writer = new Utf8JsonWriter(stream, options))
            {
                writer.WriteStartObject();
                writer.WriteString("date", DateTimeOffset.UtcNow);
                writer.WriteNumber("temp", 42);
                writer.WriteEndObject();
            }

            string json = Encoding.UTF8.GetString(stream.ToArray());
            Console.WriteLine(json);
        }
    }
}

// The example output similar to the following:
// {
//     "date": "2019-07-26T00:00:00+00:00",
//     "temp": 42
// }

Utf8JsonReader analysiert DateTime- und DateTimeOffset-Daten:

using System.Text;
using System.Text.Json;

public class Example
{
    public static void Main(string[] args)
    {
        byte[] utf8Data = Encoding.UTF8.GetBytes(@"""2019-07-26T00:00:00""");

        Utf8JsonReader json = new Utf8JsonReader(utf8Data);
        while (json.Read())
        {
            if (json.TokenType == JsonTokenType.String)
            {
                Console.WriteLine(json.TryGetDateTime(out DateTime datetime));
                Console.WriteLine(datetime);
                Console.WriteLine(json.GetDateTime());
            }
        }
    }
}

// The example displays output similar to the following:
// True
// 7/26/2019 12:00:00 AM
// 7/26/2019 12:00:00 AM

Wenn Sie versuchen, nicht konforme Formate mit Utf8JsonReader zu lesen, wird eine Formatausnahme (FormatException) ausgelöst:

using System.Text;
using System.Text.Json;

public class Example
{
    public static void Main(string[] args)
    {
        byte[] utf8Data = Encoding.UTF8.GetBytes(@"""2019/07/26 00:00:00""");

        Utf8JsonReader json = new Utf8JsonReader(utf8Data);
        while (json.Read())
        {
            if (json.TokenType == JsonTokenType.String)
            {
                Console.WriteLine(json.TryGetDateTime(out DateTime datetime));
                Console.WriteLine(datetime);

                DateTime _ = json.GetDateTime();
            }
        }
    }
}

// The example displays the following output:
// False
// 1/1/0001 12:00:00 AM
// Unhandled exception. System.FormatException: The JSON value is not in a supported DateTime format.
//     at System.Text.Json.Utf8JsonReader.GetDateTime()

Serialisieren der Eigenschaften „DateOnly“ und „TimeOnly“

Ab .NET 7 unterstützt System.Text.Json das Serialisieren und Deserialisieren von DateOnly- und TimeOnly-Typen. Sehen Sie sich das folgende Objekt an:

sealed file record Appointment(
    Guid Id,
    string Description,
    DateOnly Date,
    TimeOnly StartTime,
    TimeOnly EndTime);

Im folgenden Beispiel wird ein Appointment-Objekt serialisiert, der resultierende JSON-Code angezeigt und anschließend eine Deserialisierung in eine neue Instanz des Typs Appointment durchgeführt. Abschließend wird die ursprüngliche Instanz mit der neu deserialisierten Instanz verglichen, um zu ermitteln, ob sie gleich sind, und die Ergebnisse werden in die Konsole geschrieben:

Appointment originalAppointment = new(
    Id: Guid.NewGuid(),
    Description: "Take dog to veterinarian.",
    Date: new DateOnly(2002, 1, 13),
    StartTime: new TimeOnly(5,15),
    EndTime: new TimeOnly(5, 45));
string serialized = JsonSerializer.Serialize(originalAppointment);

Console.WriteLine($"Resulting JSON: {serialized}");

Appointment deserializedAppointment =
    JsonSerializer.Deserialize<Appointment>(serialized)!;

bool valuesAreTheSame = originalAppointment == deserializedAppointment;
Console.WriteLine($"""
    Original record has the same values as the deserialized record: {valuesAreTheSame}
    """);

Für den Code oben gilt:

  • Ein Appointment-Objekt wird instanziiert und der Variablen appointment zugewiesen.
  • Die appointment-Instanz wird mithilfe von JsonSerializer.Serialize zu JSON serialisiert.
  • Der resultierende JSON-Code wird in die Konsole geschrieben.
  • Der JSON-Code wird mithilfe von JsonSerializer.Deserialize in eine neue Instanz des Typs Appointment deserialisiert.
  • Die ursprüngliche Instanz wird mit der neu deserialisierten Instanz verglichen, um zu überprüfen, ob sie gleich sind.
  • Das Ergebnis des Vergleichs wird in die Konsole geschrieben.

Benutzerdefinierte Unterstützung von DateTime und DateTimeOffset

Bei Verwendung von JsonSerializer

Wenn das Serialisierungsmodul benutzerdefinierte Analysen oder Formatierungen vornehmen soll, können Sie benutzerdefinierte Konverter implementieren. Hier sind einige Beispiele:

DateTime(Offset).Parse und DateTime(Offset).ToString

Wenn Sie die Formate Ihrer eingegebenen DateTime- oder DateTimeOffset-Textdarstellungen nicht bestimmen können, können Sie in der Leselogik Ihres Konverters die Methode DateTime(Offset).Parse verwenden. Diese Methode ermöglicht die Nutzung der umfassenden, von .NET bereitgestellten Unterstützung für die Analyse verschiedener DateTime- und DateTimeOffset-Textformate. Hierzu zählen auch nicht ISO 8601-konforme Zeichenfolgen sowie ISO 8601-Formate, die nicht mit dem erweiterten ISO 8601-1:2019-Profil konform sind. Dieser Ansatz ist weniger leistungsfähig als die Verwendung der nativen Implementierung des Serialisierungsmoduls.

Für die Serialisierung können Sie in der Schreiblogik Ihres Konverters die Methode DateTime(Offset).ToString verwenden. Diese Methode ermöglicht das Schreiben von DateTime- und DateTimeOffset-Werten mit einem beliebigen der Standardformate für Datum und Uhrzeit sowie mit den benutzerdefinierten Datums- und Uhrzeitformaten schreiben. Dieser Ansatz ist ebenfalls weniger leistungsfähig als die Verwendung der nativen Implementierung des Serialisierungsmoduls.

using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

namespace DateTimeConverterExamples;

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

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

class Program
{
    private static void ParseDateTimeWithDefaultOptions()
    {
        DateTime _ = JsonSerializer.Deserialize<DateTime>(@"""04-10-2008 6:30 AM""");
    }

    private static void FormatDateTimeWithDefaultOptions()
    {
        Console.WriteLine(JsonSerializer.Serialize(DateTime.Parse("04-10-2008 6:30 AM -4")));
    }

    private static void ProcessDateTimeWithCustomConverter()
    {
        JsonSerializerOptions options = new JsonSerializerOptions();
        options.Converters.Add(new DateTimeConverterUsingDateTimeParse());

        string testDateTimeStr = "04-10-2008 6:30 AM";
        string testDateTimeJson = @"""" + testDateTimeStr + @"""";

        DateTime resultDateTime = JsonSerializer.Deserialize<DateTime>(testDateTimeJson, options);
        Console.WriteLine(resultDateTime);

        string resultDateTimeJson = JsonSerializer.Serialize(DateTime.Parse(testDateTimeStr), options);
        Console.WriteLine(Regex.Unescape(resultDateTimeJson));
    }

    static void Main(string[] args)
    {
        // Parsing non-compliant format as DateTime fails by default.
        try
        {
            ParseDateTimeWithDefaultOptions();
        }
        catch (JsonException e)
        {
            Console.WriteLine(e.Message);
        }

        // Formatting with default options prints according to extended ISO 8601 profile.
        FormatDateTimeWithDefaultOptions();

        // Using converters gives you control over the serializers parsing and formatting.
        ProcessDateTimeWithCustomConverter();
    }
}

// The example displays output similar to the following:
// The JSON value could not be converted to System.DateTime. Path: $ | LineNumber: 0 | BytePositionInLine: 20.
// "2008-04-10T06:30:00-04:00"
// 4/10/2008 6:30:00 AM
// "4/10/2008 6:30:00 AM"

Hinweis

Wenn Sie JsonConverter<T> implementieren und es sich bei T um DateTime handelt, ist der Parameter typeToConvert immer typeof(DateTime). Der Parameter ist nützlich für die Behandlung polymorpher Fälle sowie bei Verwendung von Generics, um bei der Ermittlung von typeof(T) eine hohe Leistung zu erzielen.

Utf8Parser und Utf8Formatter

Sie können schnelle, UTF-8-basierte Analyse- und Formatierungsmethoden in Ihrer Konverterlogik verwenden, wenn Ihre eingegebenen DateTime- oder DateTimeOffset-Textdarstellungen mit einer der Standardformatzeichenfolgen für Datum und Uhrzeit („R“, „l“, „O“ oder „G“) kompatibel sind oder Sie Schreibvorgänge ausführen möchten, die einem dieser Formate entsprechen. Dieser Ansatz ist viel schneller als die Verwendung von DateTime(Offset).Parse und DateTime(Offset).ToString.

Das folgende Beispiel zeigt einen benutzerdefinierten Konverter, der DateTime-Werte gemäß dem Standardformat „R“ serialisiert und deserialisiert:

using System.Buffers;
using System.Buffers.Text;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace DateTimeConverterExamples;

// This converter reads and writes DateTime values according to the "R" standard format specifier:
// https://learn.microsoft.com/dotnet/standard/base-types/standard-date-and-time-format-strings#the-rfc1123-r-r-format-specifier.
public class DateTimeConverterForCustomStandardFormatR : JsonConverter<DateTime>
{
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        Debug.Assert(typeToConvert == typeof(DateTime));

        if (Utf8Parser.TryParse(reader.ValueSpan, out DateTime value, out _, 'R'))
        {
            return value;
        }

        throw new FormatException();
    }

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        // The "R" standard format will always be 29 bytes.
        Span<byte> utf8Date = new byte[29];

        bool result = Utf8Formatter.TryFormat(value, utf8Date, out _, new StandardFormat('R'));
        Debug.Assert(result);

        writer.WriteStringValue(utf8Date);
    }
}

class Program
{
    private static void ParseDateTimeWithDefaultOptions()
    {
        DateTime _ = JsonSerializer.Deserialize<DateTime>(@"""Thu, 25 Jul 2019 13:36:07 GMT""");
    }

    private static void ProcessDateTimeWithCustomConverter()
    {
        JsonSerializerOptions options = new JsonSerializerOptions();
        options.Converters.Add(new DateTimeConverterForCustomStandardFormatR());

        string testDateTimeStr = "Thu, 25 Jul 2019 13:36:07 GMT";
        string testDateTimeJson = @"""" + testDateTimeStr + @"""";

        DateTime resultDateTime = JsonSerializer.Deserialize<DateTime>(testDateTimeJson, options);
        Console.WriteLine(resultDateTime);

        Console.WriteLine(JsonSerializer.Serialize(DateTime.Parse(testDateTimeStr), options));
    }

    static void Main(string[] args)
    {
        // Parsing non-compliant format as DateTime fails by default.
        try
        {
            ParseDateTimeWithDefaultOptions();
        }
        catch (JsonException e)
        {
            Console.WriteLine(e.Message);
        }

        // Using converters gives you control over the serializers parsing and formatting.
        ProcessDateTimeWithCustomConverter();
    }
}

// The example displays output similar to the following:
// The JSON value could not be converted to System.DateTime.Path: $ | LineNumber: 0 | BytePositionInLine: 31.
// 7/25/2019 1:36:07 PM
// "Thu, 25 Jul 2019 09:36:07 GMT"

Hinweis

Das Standardformat „R“ ist immer 29 Zeichen lang.

Das Format „l“ (kleines L) ist nicht zusammen mit den anderen Standardformatzeichenfolgen für Datum und Uhrzeit dokumentiert, da es nur von den Typen Utf8Parser und Utf8Formatter unterstützt wird. Das Format entspricht RFC 1123 in Kleinbuchstaben (ist also eine Kleinbuchstabenversion des R-Formats). Beispiel: "thu, 25 jul 2019 06:36:07 gmt".

Verwenden Sie DateTime(Offset). Analysieren als Fallback

Wenn Sie im Allgemeinen davon ausgehen, dass Ihre DateTime- oder DateTimeOffset-Eingabedaten dem erweiterten ISO 8601-1:2019-Profil entsprechen, können Sie die native Analyselogik des Serialisierungsmoduls verwenden. Sie können auch einen Fallbackmechanismus implementieren. Das folgende Beispiel zeigt, dass der Konverter die Daten erfolgreich mithilfe von DateTime analysiert, nachdem die Analyse einer TryGetDateTime(DateTime)-Textdarstellung mithilfe von Parse(String) nicht erfolgreich war:

using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

namespace DateTimeConverterExamples;

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

        if (!reader.TryGetDateTime(out DateTime value))
        {
            value = DateTime.Parse(reader.GetString()!);
        }

        return value;
    }

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString("dd/MM/yyyy"));
    }
}

class Program
{
    private static void ParseDateTimeWithDefaultOptions()
    {
        DateTime _ = JsonSerializer.Deserialize<DateTime>(@"""2019-07-16 16:45:27.4937872+00:00""");
    }

    private static void ProcessDateTimeWithCustomConverter()
    {
        JsonSerializerOptions options = new JsonSerializerOptions();
        options.Converters.Add(new DateTimeConverterUsingDateTimeParseAsFallback());

        string testDateTimeStr = "2019-07-16 16:45:27.4937872+00:00";
        string testDateTimeJson = @"""" + testDateTimeStr + @"""";

        DateTime resultDateTime = JsonSerializer.Deserialize<DateTime>(testDateTimeJson, options);
        Console.WriteLine(resultDateTime);

        string resultDateTimeJson = JsonSerializer.Serialize(DateTime.Parse(testDateTimeStr), options);
        Console.WriteLine(Regex.Unescape(resultDateTimeJson));
    }

    static void Main(string[] args)
    {
        // Parsing non-compliant format as DateTime fails by default.
        try
        {
            ParseDateTimeWithDefaultOptions();
        }
        catch (JsonException e)
        {
            Console.WriteLine(e.Message);
        }

        // Using converters gives you control over the serializers parsing and formatting.
        ProcessDateTimeWithCustomConverter();
    }
}

// The example displays output similar to the following:
// The JSON value could not be converted to System.DateTime.Path: $ | LineNumber: 0 | BytePositionInLine: 35.
// 7/16/2019 4:45:27 PM
// "16/07/2019"

Verwenden Sie das Unix-Epochendatumsformat.

Die folgenden Konverter behandeln das Unix-Epochenformat mit oder ohne Zeitzonenoffset (Werte wie /Date(1590863400000-0700)/ oder /Date(1590863400000)/):

sealed class UnixEpochDateTimeOffsetConverter : JsonConverter<DateTimeOffset>
{
    static readonly DateTimeOffset s_epoch = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
    static readonly Regex s_regex = new("^/Date\\(([+-]*\\d+)([+-])(\\d{2})(\\d{2})\\)/$", RegexOptions.CultureInvariant);

    public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        string formatted = reader.GetString()!;
        Match match = s_regex.Match(formatted);

        if (
                !match.Success
                || !long.TryParse(match.Groups[1].Value, System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture, out long unixTime)
                || !int.TryParse(match.Groups[3].Value, System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture, out int hours)
                || !int.TryParse(match.Groups[4].Value, System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture, out int minutes))
        {
            throw new JsonException();
        }

        int sign = match.Groups[2].Value[0] == '+' ? 1 : -1;
        TimeSpan utcOffset = new(hours * sign, minutes * sign, 0);

        return s_epoch.AddMilliseconds(unixTime).ToOffset(utcOffset);
    }

    public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
    {
        long unixTime = Convert.ToInt64((value - s_epoch).TotalMilliseconds);
        TimeSpan utcOffset = value.Offset;

        string formatted = string.Create(CultureInfo.InvariantCulture, $"/Date({unixTime}{(utcOffset >= TimeSpan.Zero ? "+" : "-")}{utcOffset:hhmm})/");

        writer.WriteStringValue(formatted);
    }
}
sealed class UnixEpochDateTimeConverter : JsonConverter<DateTime>
{
    static readonly DateTime s_epoch = new(1970, 1, 1, 0, 0, 0);
    static readonly Regex s_regex = new("^/Date\\(([+-]*\\d+)\\)/$", RegexOptions.CultureInvariant);

    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        string formatted = reader.GetString()!;
        Match match = s_regex.Match(formatted);

        if (
                !match.Success
                || !long.TryParse(match.Groups[1].Value, System.Globalization.NumberStyles.Integer, CultureInfo.InvariantCulture, out long unixTime))
        {
            throw new JsonException();
        }

        return s_epoch.AddMilliseconds(unixTime);
    }

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        long unixTime = Convert.ToInt64((value - s_epoch).TotalMilliseconds);

        string formatted = string.Create(CultureInfo.InvariantCulture, $"/Date({unixTime})/");
        writer.WriteStringValue(formatted);
    }
}

Bei Verwendung von Utf8JsonWriter

Wenn Sie eine benutzerdefinierte DateTime- oder DateTimeOffset-Textdarstellung mit Utf8JsonWriter schreiben möchten, können Sie Ihre benutzerdefinierte Darstellung als String, ReadOnlySpan<Byte>, ReadOnlySpan<Char> oder JsonEncodedTextformatieren und sie anschließend an die entsprechende Utf8JsonWriter.WriteStringValue- oder Utf8JsonWriter.WriteString-Methode übergeben.

Im folgenden Beispiel wird gezeigt, wie ein benutzerdefiniertes DateTime-Format mit ToString(String, IFormatProvider) erstellt und anschließend mit der Methode WriteStringValue(String) geschrieben werden kann:

using System.Globalization;
using System.Text;
using System.Text.Json;

public class Example
{
    public static void Main(string[] args)
    {
        var options = new JsonWriterOptions
        {
            Indented = true
        };

        using (var stream = new MemoryStream())
        {
            using (var writer = new Utf8JsonWriter(stream, options))
            {
                string dateStr = DateTime.UtcNow.ToString("F", CultureInfo.InvariantCulture);

                writer.WriteStartObject();
                writer.WriteString("date", dateStr);
                writer.WriteNumber("temp", 42);
                writer.WriteEndObject();
            }

            string json = Encoding.UTF8.GetString(stream.ToArray());
            Console.WriteLine(json);
        }
    }
}

// The example displays output similar to the following:
// {
//     "date": "Tuesday, 27 August 2019 19:21:44",
//     "temp": 42
// }

Bei Verwendung von Utf8JsonReader

Wenn Sie eine benutzerdefinierte DateTime- oder DateTimeOffset-Textdarstellung mit Utf8JsonReader lesen möchten, können Sie den Wert des aktuellen JSON-Tokens mithilfe der Methode GetString() als String abrufen und ihn anschließend mithilfe von benutzerdefinierter Logik analysieren.

Im folgenden Beispiel wird gezeigt, wie eine benutzerdefinierte DateTimeOffset-Textdarstellung mithilfe der Methode GetString() abgerufen und anschließend mithilfe von ParseExact(String, String, IFormatProvider) analysiert werden kann:

using System.Globalization;
using System.Text;
using System.Text.Json;

public class Example
{
    public static void Main(string[] args)
    {
        byte[] utf8Data = Encoding.UTF8.GetBytes(@"""Friday, 26 July 2019 00:00:00""");

        var json = new Utf8JsonReader(utf8Data);
        while (json.Read())
        {
            if (json.TokenType == JsonTokenType.String)
            {
                string value = json.GetString();
                DateTimeOffset dto = DateTimeOffset.ParseExact(value, "F", CultureInfo.InvariantCulture);
                Console.WriteLine(dto);
            }
        }
    }
}

// The example displays output similar to the following:
// 7/26/2019 12:00:00 AM -04:00

Das erweiterte ISO 8601-1:2019-Profil in „System.Text.Json“

Datums- und Uhrzeitkomponenten

Das in System.Text.Json implementierte erweiterte ISO 8601-1:2019-Profil definiert folgende Komponenten für Datums- und Uhrzeitdarstellungen. Diese Komponenten werden verwendet, um verschiedene Granularitätsebenen zu definieren, die bei der Analyse und Formatierung von DateTime- und DateTimeOffset-Darstellungen unterstützt werden.

Komponente Format Beschreibung
Year "yyyy" 0001–9999
Monat "MM" 01–12
Tag "dd" 01–28, 01–29, 01–30, 01–31 (basierend auf Monat/Jahr)
Stunde "HH" 00–23
Minute "mm" 00–59
Second "ss" 00–59
Zweite Bruchzahl "FFFFFFF" Mindestens eine Stelle, maximal 16 Stellen
Zeitoffset "K" Entweder "Z" oder "('+'/'-')HH':'mm".
Partielle Zeit "HH':'mm':'ss[FFFFFFF]" Zeit ohne UTC-Abweichungsinformationen
Vollständiges Datum "jjjj'-'MM'-'tt" Kalenderdatum
Vollständige Zeit "'Partielle Zeit'K" UTC des Tages oder lokale Tageszeit mit dem Zeitoffset zwischen lokaler Zeit und UTC
Datum und Uhrzeit "'Vollständiges Datum''T''Vollständige Zeit'" Kalenderdatum und Tageszeit, z. B. 2019-07-26T16:59:57-05:00

Unterstützung für die Analyse

Für die Analyse sind folgende Granularitätsebenen definiert:

  1. 'Vollständiges Datum'

    1. "jjjj'-'MM'-'tt"
  2. "'Vollständiges Datum''T''Stunde'':''Minute'"

    1. "jjjj'-'MM'-'tt'T'HH':'mm"
  3. "'Vollständiges Datum''T''Partielle Zeit'"

    1. "jjjj'-'MM'-'tt'T'HH':'mm':'ss" (Der Bezeichner „s“ für sortierbares Format)
    2. "jjjj'-'MM'-'tt'T'HH':'mm':'ss'.'FFFFFFF"
  4. "'Vollständiges Datum''T''Uhrzeit Stunde'':''Minute''Zeitoffset'"

    1. "jjjj'-'MM'-'tt'T'HH':'mmZ"
    2. "jjjj'-'MM'-'tt'T'HH':'mm('+'/'-')HH':'mm"
  5. 'Datum und Uhrzeit'

    1. "jjjj'-'MM'-'tt'T'HH':'mm':'ssZ"
    2. "jjjj'-'MM'-'tt'T'HH':'mm':'ss'.'FFFFFFFZ"
    3. "jjjj'-'MM'-'tt'T'HH':'mm':'ss('+'/'-')HH':'mm"
    4. "jjjj'-'MM'-'tt'T'HH':'mm':'ss'.'FFFFFFF('+'/'-')HH':'mm"

    Diese Granularitätsebene entspricht RFC 3339, einem weit verbreiteten Profil von ISO 8601, das zum Austauschen von Datums- und Uhrzeitinformationen verwendet wird. Es gibt allerdings ein paar Einschränkungen bei der Implementierung von System.Text.Json.

    • RFC 3339 gibt keine maximale Anzahl von Stellen für Sekundenbruchteile an. Stattdessen lautet die Vorgabe, dass nach dem Komma mindestens eine Stelle folgen muss, wenn ein Abschnitt für Sekundenbruchteile vorhanden ist. Die Implementierung in System.Text.Json ermöglicht die Verwendung von bis zu 16 Stellen (zur Unterstützung der Interoperabilität mit anderen Programmiersprachen und Frameworks). Analysiert werden jedoch nur die ersten sieben Stellen. Wenn beim Lesen von DateTime- und DateTimeOffset-Instanzen mehr als 16 Stellen für Sekundenbruchteile vorhanden sind, wird eine JSON-Ausnahme (JsonException) ausgelöst.
    • Gemäß RFC 3339 können die Zeichen „T“ und „Z“ auch „t“ bzw. „z“ sein. Anwendungen können die Unterstützung jedoch auf die großgeschriebenen Varianten beschränken. Bei der Implementierung in System.Text.Json müssen sie „T“ und „Z“ lauten. Wenn Eingabenutzdaten beim Lesen von DateTime- und DateTimeOffset-Instanzen „t“ oder „z“ enthalten, wird eine JSON-Ausnahme (JsonException) ausgelöst.
    • Gemäß RFC 3339 werden der Datums- und der Uhrzeitabschnitt durch „T“ getrennt. Anwendungen können als Trennzeichen aber auch ein Leerzeichen („ “) verwenden. Bei System.Text.Json müssen der Datums- und der Uhrzeitabschnitt durch ein „T“ getrennt werden. Wenn Eingabenutzdaten beim Lesen von DateTime- und DateTimeOffset-Instanzen ein Leerzeichen („ “) enthalten, wird eine JSON-Ausnahme (JsonException) ausgelöst.

Wenn Dezimalbrüche für Sekunden vorhanden sind, muss mindestens eine Stelle vorhanden sein. 2019-07-26T00:00:00. ist nicht zulässig. Es sind zwar bis zu 16 Nachkommastellen zulässig, aber nur die ersten sieben werden analysiert. Darüber hinausgehende Stellen werden als null betrachtet. 2019-07-26T00:00:00.1234567890 wird beispielsweise als 2019-07-26T00:00:00.1234567 analysiert. Dieser Ansatz gewährleistet die Kompatibilität mit der DateTime-Implementierung, die auf diese Auflösung beschränkt ist.

Schaltsekunden werden nicht unterstützt.

Formatierungsunterstützung

Für die Formatierung sind folgende Granularitätsebenen definiert:

  1. "'Vollständiges Datum''T''Partielle Zeit'"

    1. "jjjj'-'MM'-'tt'T'HH':'mm':'ss" (Der Bezeichner „s“ für sortierbares Format)

      Wird verwendet, um einen DateTime-Wert ohne Sekundenbruchteile und ohne Offsetinformationen zu formatieren.

    2. "jjjj'-'MM'-'tt'T'HH':'mm':'ss'.'FFFFFFF"

      Wird verwendet, um einen DateTime-Wert mit Sekundenbruchteilen, aber ohne Offsetinformationen zu formatieren.

  2. 'Datum und Uhrzeit'

    1. "jjjj'-'MM'-'tt'T'HH':'mm':'ssZ"

      Wird verwendet, um einen DateTime-Wert ohne Sekundenbruchteile, aber mit einer UTC-Abweichung zu formatieren.

    2. "jjjj'-'MM'-'tt'T'HH':'mm':'ss'.'FFFFFFFZ"

      Wird verwendet, um einen DateTime-Wert mit Sekundenbruchteilen und mit einer UTC-Abweichung zu formatieren.

    3. "jjjj'-'MM'-'tt'T'HH':'mm':'ss('+'/'-')HH':'mm"

      Wird verwendet, um einen DateTime oder DateTimeOffset-Wert ohne Sekundenbruchteile, aber mit einem lokalen Offset zu formatieren.

    4. "jjjj'-'MM'-'tt'T'HH':'mm':'ss'.'FFFFFFF('+'/'-')HH':'mm"

      Wird verwendet, um einen DateTime oder DateTimeOffset-Wert mit Sekundenbruchteilen und mit einem lokalen Offset zu formatieren.

    Diese Granularitätsebene ist mit RFC 3339 konform.

Wenn die Darstellung einer DateTime- oder DateTimeOffset-Instanz im Roundtripformat nachgestellte Nullen in den Sekundenbruchteilen enthält, formatieren JsonSerializer und Utf8JsonWriter eine Darstellung der Instanz ohne nachgestellte Nullen. So wird beispielsweise eine DateTime-Instanz, die im Roundtripformat als 2019-04-24T14:50:17.1010000Z dargestellt wird, von JsonSerializer und Utf8JsonWriter als 2019-04-24T14:50:17.101Z formatiert.

Wenn die Darstellung einer DateTime- oder DateTimeOffset-Instanz im Roundtripformat nur Nullen in den Sekundenbruchteilen enthält, formatieren JsonSerializer und Utf8JsonWriter eine Darstellung der Instanz ohne Sekundenbruchteile. So wird beispielsweise eine DateTime-Instanz, die im Roundtripformat als 2019-04-24T14:50:17.0000000+02:00 dargestellt wird, von JsonSerializer und Utf8JsonWriter als 2019-04-24T14:50:17+02:00 formatiert.

Durch das Abschneiden von Nullen in Sekundenbruchteilen kann die kleinstmögliche Ausgabe geschrieben werden, die zur Erhaltung der Informationen eines Roundtrips erforderlich ist.

Es werden maximal sieben Stellen für Sekundenbruchteile geschrieben. Dieses Maximum orientiert sich an der DateTime-Implementierung, die auf diese Auflösung beschränkt ist.