Supporto di DateTime e DateTimeOffset in System.Text.Json

La libreria System.Text.Json analizza e scrive i valori DateTime e DateTimeOffset in base al profilo completo ISO 8601-1:2019. I convertitori forniscono supporto personalizzato per la serializzazione e la deserializzazione con JsonSerializer. È anche possibile usare Utf8JsonReader e Utf8JsonWriter per implementare il supporto personalizzato.

Supporto per il formato ISO 8601-1:2019

I tipi JsonSerializer, Utf8JsonReader, Utf8JsonWriter e JsonElement analizzano e scrivono le rappresentazioni di testo DateTime e DateTimeOffset in base al profilo completo del formato ISO 8601-1:2019. Ad esempio: 2019-07-26T16:59:57-05:00.

I dati DateTime e DateTimeOffset possono essere serializzati con JsonSerializer:

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"}

DateTime e DateTimeOffset possono anche essere deserializzati con JsonSerializer:

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

Con le opzioni predefinite, le rappresentazioni di testo DateTime e DateTimeOffset di input devono essere conformi al profilo ISO 8601-1:2019 completo. Il tentativo di deserializzare le rappresentazioni non conformi al profilo porterà JsonSerializer a generare JsonException:

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.

JsonDocument fornisce accesso strutturato al contenuto di un payload JSON, incluse le rappresentazioni DateTime e DateTimeOffset. Nell'esempio seguente viene illustrato come calcolare la temperatura media del lunedì da una raccolta di temperature:

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

Il tentativo di calcolare la temperatura media in base a un payload con rappresentazioni DateTime non conformi porterà JsonDocument a generare FormatException:

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()

Il livello Utf8JsonWriter inferiore scrive i dati DateTime e DateTimeOffset:

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 analizza i dati DateTime e DateTimeOffset:

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

Il tentativo di leggere formati non conformi con Utf8JsonReader porterà a generare FormatException:

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()

Serializzare le proprietà DateOnly e TimeOnly

Con .NET 7+, System.Text.Json supporta la serializzazione e la deserializzazione dei tipi DateOnly e TimeOnly. Si consideri l'oggetto seguente:

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

L'esempio seguente serializza un oggetto Appointment, visualizza il JSON risultante e successivamente lo deserializza nuovamente in una nuova istanza del tipo Appointment. Infine, le istanze originali e appena deserializzate vengono confrontate per verificarne l'uguaglianza e i risultati vengono scritti nella console:

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

Nel codice precedente:

  • Viene creata un'istanza di un oggetto Appointment e assegnata alla variabile appointment.
  • L'istanza appointment viene serializzata in JSON usando JsonSerializer.Serialize.
  • Il JSON risultante viene scritto nella console.
  • Il JSON viene deserializzato nuovamente in una nuova istanza del tipo Appointment usando JsonSerializer.Deserialize.
  • Le istanze originali e appena deserializzate vengono confrontate per verificarne l'uguaglianza.
  • Il risultato del confronto viene scritto nella console.

Supporto personalizzato per DateTime e DateTimeOffset

Quando si utilizza JsonSerializer

Se si desidera che il serializzatore esegua l'analisi o la formattazione personalizzata, è possibile implementare i convertitori personalizzati. Ecco alcuni esempi:

DateTime(Offset).Parse e DateTime(Offset).ToString

Se non è possibile determinare i formati delle rappresentazioni di testo DateTime o DateTimeOffset di input, è possibile usare il metodo DateTime(Offset).Parse nella logica di lettura del convertitore. Questo metodo consente di usare il supporto completo di NET per l'analisi di vari formati di testo DateTime e DateTimeOffset, tra cui stringhe non ISO 8601 e formati ISO 8601 non conformi al profilo ISO 8601-1:2019 completo. Questo approccio è meno efficiente rispetto all'uso dell'implementazione nativa del serializzatore.

Per la serializzazione, è possibile usare il metodo DateTime(Offset).ToString nella logica di scrittura del convertitore. Questo metodo consente di scrivere i valori DateTime e DateTimeOffset usando uno qualsiasi dei formati di data e ora standard e i formati di data e ora personalizzati. Questo approccio è anche meno efficiente rispetto all'uso dell'implementazione nativa del serializzatore.

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"

Nota

Quando si implementa JsonConverter<T>, e T è DateTime, il parametro typeToConvert sarà sempre typeof(DateTime). Il parametro è utile per gestire i casi polimorfici e quando si usa generics per ottenere typeof(T) in modo efficiente.

Utf8Parser e Utf8Formatter

È possibile usare metodi di analisi e formattazione basati su UTF-8 veloci nella logica del convertitore se le rappresentazioni di testo DateTime o DateTimeOffset di input sono conformi a una delle stringhe di formato di data e ora standard "R", "l", "O" o "G" oppure si vuole scrivere in base a uno di questi formati. Questo approccio è molto più veloce rispetto all'uso di DateTime(Offset).Parse e DateTime(Offset).ToString.

L'esempio seguente mostra un convertitore personalizzato che serializza e deserializza i valor DateTimei in base al formato standard "R":

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"

Nota

Il formato standard "R" avrà sempre una lunghezza di 29 caratteri.

Il formato "l" (minuscolo "L") non è documentato con le altre stringhe di formato data e ora standard perché è supportato solo dai tipi Utf8Parser e Utf8Formatter. Il formato è RFC 1123 minuscolo (una versione minuscola del formato "R"). Ad esempio, "thu, 25 jul 2019 06:36:07 gmt".

Usare DateTime(Offset).Parse come fallback

Se in genere si prevede che i dati DateTime o DateTimeOffset di input siano conformi al profilo ISO 8601-1:2019 completo, è possibile usare la logica di analisi nativa del serializzatore. È anche possibile implementare un meccanismo di fallback. L'esempio seguente mostra che, dopo la mancata esecuzione dell'analisi di una rappresentazione di testo DateTime usando TryGetDateTime(DateTime), il convertitore analizza correttamente i dati usando Parse(String):

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"

Usare il formato Unix epoch date

I convertitori seguenti gestiscono il formato Unix epoch con o senza una differenza di fuso orario (valori come /Date(1590863400000-0700)/ o /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);
    }
}

Quando si utilizza Utf8JsonWriter

Se si vuole scrivere una rappresentazione di testo DateTime o DateTimeOffset personalizzata con Utf8JsonWriter, è possibile formattare la rappresentazione personalizzata in String, ReadOnlySpan<Byte>, ReadOnlySpan<Char> o JsonEncodedText, quindi passarla al metodo Utf8JsonWriter.WriteStringValue o Utf8JsonWriter.WriteString corrispondente.

L'esempio seguente illustra come creare un formato personalizzato DateTime con ToString(String, IFormatProvider) e quindi scriverlo con il metodo WriteStringValue(String):

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
// }

Quando si utilizza Utf8JsonReader

Se si vuole leggere una rappresentazione di testo DateTime o DateTimeOffset personalizzata con Utf8JsonReader, è possibile ottenere il valore del token JSON corrente come String usando il metodo GetString(), quindi analizzare il valore usando la logica personalizzata.

Nell'esempio seguente viene illustrato come recuperare una rappresentazione di testo DateTimeOffset personalizzata usando il metodo GetString(), quindi analizzarla usando ParseExact(String, String, IFormatProvider):

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

Profilo ISO 8601-1:2019 completo in System.Text.Json

Componenti di data e ora

Il profilo ISO 8601-1:2019 completo implementato in System.Text.Json definisce i componenti seguenti per le rappresentazioni di data e ora. Questi componenti vengono usati per definire vari livelli di granularità supportati durante l'analisi e la formattazione delle rappresentazioni. DateTime e DateTimeOffset.

Componente Formato Descrizione
Year "yyyy" 0001-9999
Mese "MM" 01-12
Giornaliera "dd" 01-28, 01-29, 01-30, 01-31 in base al mese/anno.
Ore "HH" 00-23
Minute "mm" 00-59
Secondo "ss" 00-59
Seconda frazione "FFFFFFF" Minimo di una cifra, massimo di 16 cifre.
Differenza ora "K" "Z" o "('+'/'-')HH':'mm".
Ora parziale "HH':'mm':'ss[FFFFFFF]" Ora senza informazioni sulla differenza dall'ora UTC.
Data completa "aaaa'-'MM'-'gg" Data calendario.
Ora completa "Tempo parziale"K" UTC del giorno o ora locale del giorno con la differenza di ora tra l'ora locale e l'ora UTC.
Data e ora "'Data completa''T''Ora completa'" Data e ora del giorno del calendario, ad esempio 2019-07-26T16:59:57-05:00.

Supporto per l'analisi

Per l'analisi vengono definiti i livelli di granularità seguenti:

  1. "Data completa"

    1. "aaaa'-'MM'-'gg"
  2. "Data completa''T''Ora'':"Minuti'"

    1. "aaaa"-"MM"-"gg"T"HH":"mm"
  3. "Data completa''T''Ora parziale"

    1. "aaaa"-"MM"-"gg"T"HH":"mm":"ss" (Identificatore di formato ordinabile ("s"))
    2. "aaaa"-"MM"-"gg"T"HH":"mm":"ss"."FFFFFFF"
  4. "Data completa''T''Ora'':''Minuti''Differenza ora"

    1. "aaaa"-"MM"-"gg"T"HH":"mmZ"
    2. "aaaa"-"MM"-"gg"T"HH":"mm("+"/"-")HH":"mm"
  5. "Data ora"

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

    Questo livello di granularità è conforme a RFC 3339, un profilo ampiamente adottato di ISO 8601 usato per scambiare le informazioni di data e ora. Esistono tuttavia alcune restrizioni nell'implementazione di System.Text.Json.

    • RFC 3339 non specifica un numero massimo di cifre in secondi frazionari, ma specifica che almeno una cifra deve seguire il punto, se è presente una sezione di secondi frazionari. L'implementazione in System.Text.Json consente fino a 16 cifre (per supportare l'interoperabilità con altri linguaggi e framework di programmazione), ma analizza solo i primi sette. Verrà generata un'eccezione JsonException se sono presenti più di 16 cifre di secondo frazionari durante la lettura delle istanze DateTime e DateTimeOffset.
    • RFC 3339 consente rispettivamente ai caratteri "T" e "Z" di essere "t" o "z", ma consente alle applicazioni di limitare il supporto solo alle varianti in maiuscolo. L'implementazione in System.Text.Json richiede che siano "T" e "Z". Verrà generata un'eccezione JsonException se i payload di input contengono "t" o "z" durante la lettura delle istanze DateTime e DateTimeOffset.
    • RFC 3339 specifica che le sezioni di data e ora sono separate da "T", ma consente alle applicazioni di separarle con uno spazio (" "). System.Text.Json richiede che le sezioni di data e ora siano separate con "T". Verrà generata un'eccezione JsonException se i payload di input contengono uno spazio (" ") durante la lettura delle istanze DateTime e DateTimeOffset.

Se sono presenti frazioni decimali per i secondi, deve essere presente almeno una cifra. 2019-07-26T00:00:00. non è consentito. Sebbene siano consentite fino a 16 cifre frazionarie, vengono analizzate solo le prime sette. Tutte le altre sono considerate pari a uno zero. Ad esempio, 2019-07-26T00:00:00.1234567890 verrà analizzato come se fosse 2019-07-26T00:00:00.1234567. Questo approccio mantiene la compatibilità con l'implementazione DateTime, limitata a questa risoluzione.

I secondi intercalari non sono supportati.

Supporto per la formattazione

Per la formattazione vengono definiti i livelli di granularità seguenti:

  1. "Data completa''T''Ora parziale"

    1. "aaaa"-"MM"-"gg"T"HH":"mm":"ss" (Identificatore di formato ordinabile ("s"))

      Utilizzato per formattare DateTime senza secondi frazionari e senza informazioni sulla differenza.

    2. "aaaa"-"MM"-"gg"T"HH":"mm":"ss"."FFFFFFF"

      Utilizzato per formattare DateTime con secondi frazionari ma senza informazioni sulla differenza.

  2. "Data ora"

    1. "aaaa"-"MM"-"gg"T"HH":"mm":"ssZ"

      Utilizzato per formattare DateTime senza secondi frazionari, ma con una differenza dall'ora UTC.

    2. "aaaa"-"MM"-"gg"T"HH":"mm":"ss"."FFFFFFFZ"

      Utilizzato per formattare DateTime senza secondi frazionari e con una differenza dall'ora UTC.

    3. "aaaa"-"MM"-"gg"T"HH":"mm":"ss("+"/"-")HH":"mm"

      Utilizzato per formattare DateTime o DateTimeOffset senza secondi frazionari, ma con una differenza dall'ora locale.

    4. "aaaa"-"MM"-"gg"T"HH":"mm":"ss"."FFFFFFF("+"/"-")HH":"mm"

      Utilizzato per formattare DateTime o DateTimeOffset senza secondi frazionari e con una differenza dall'ora locale.

    Questo livello di granularità è conforme a RFC 3339.

Se la rappresentazione in formato round trip di un'istanza DateTime o DateTimeOffset ha zeri finali nei secondi frazionari, JsonSerializer e Utf8JsonWriter formattano una rappresentazione dell'istanza senza zeri finali. Ad esempio, un'istanza DateTime la cui rappresentazione in formato round trip è 2019-04-24T14:50:17.1010000Z, verrà formattata come 2019-04-24T14:50:17.101Z da JsonSerializer e Utf8JsonWriter.

Se la rappresentazione in formato round trip di un'istanza DateTime o DateTimeOffset ha tutti gli zeri nei secondi frazionari, JsonSerializer e Utf8JsonWriter formattano una rappresentazione dell'istanza senza secondi frazionari. Ad esempio, un'istanza DateTime la cui rappresentazione in formato round trip è 2019-04-24T14:50:17.0000000+02:00, verrà formattata come 2019-04-24T14:50:17+02:00 da JsonSerializer e Utf8JsonWriter.

Il troncamento degli zeri nelle cifre in secondi frazionari consente la scrittura dell'output più piccolo necessario per conservare le informazioni su un round trip.

Vengono scritte al massimo sette cifre in secondi frazionari. Questo valore massimo è allineato all'implementazione DateTime, limitata a questa risoluzione.