DateTime and DateTimeOffset support in System.Text.Json

The System.Text.Json library parses and writes DateTime and DateTimeOffset values according to the ISO 8601:-2019 extended profile. Converters provide custom support for serializing and deserializing with JsonSerializer. Custom support can also be implemented when using Utf8JsonReader and Utf8JsonWriter.

Support for the ISO 8601-1:2019 format

The JsonSerializer, Utf8JsonReader, Utf8JsonWriter, and JsonElement types parse and write DateTime and DateTimeOffset text representations according to the extended profile of the ISO 8601-1:2019 format; for example, 2019-07-26T16:59:57-05:00.

DateTime and DateTimeOffset data can be serialized with JsonSerializer:

using System;
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);
    }
}

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

They can also be deserialized with JsonSerializer:

using System;
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 the following output:
// Banana
// 7/26/2019 12:00:00 AM

With default options, input DateTime and DateTimeOffset text representations must conform to the extended ISO 8601-1:2019 profile. Attempting to deserialize representations that don't conform to the profile will cause JsonSerializer to throw a JsonException:

using System;
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.

The JsonDocument provides structured access to the contents of a JSON payload, including DateTime and DateTimeOffset representations. The example below shows how, when given a collection of temperatures, the average temperature on Mondays can be calculated:

using System;
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

Attempting to compute the average temperature given a payload with non-compliant DateTime representations will cause JsonDocument to throw a FormatException:

using System;
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()

The lower level Utf8JsonWriter writes DateTime and DateTimeOffset data:

using System;
using System.IO;
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 displays the following output:
// {
//     "date": "2019-07-26T00:00:00-05:00",
//     "temp": 42
// }

Utf8JsonReader parses DateTime and DateTimeOffset data:

using System;
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 the following output:
// True
// 7/26/2019 12:00:00 AM
// 7/26/2019 12:00:00 AM

Attempting to read non-compliant formats with Utf8JsonReader will cause it to throw a FormatException:

using System;
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()

Custom support for DateTime and DateTimeOffset

When using JsonSerializer

If you want the serializer to perform custom parsing or formatting, you can implement custom converters. Here are a few examples:

Using DateTime(Offset).Parse and DateTime(Offset).ToString

If you can't determine the formats of your input DateTime or DateTimeOffset text representations, you can use the DateTime(Offset).Parse method in your converter read logic. This allows you to use .NET's extensive support for parsing various DateTime and DateTimeOffset text formats, including non-ISO 8601 strings and ISO 8601 formats that don't conform to the extended ISO 8601-1:2019 profile. This approach is significantly less performant than using the serializer's native implementation.

For serializing, you can use the DateTime(Offset).ToString method in your converter write logic. This allows you to write DateTime and DateTimeOffset values using any of the standard date and time formats, and the custom date and time formats. This is also significantly less performant than using the serializer's native implementation.

using System;
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());
        }

        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 the following output:
// 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"

Note

When implementing JsonConverter<T>, and T is DateTime, the typeToConvert parameter will always be typeof(DateTime). The parameter is useful for handling polymorphic cases and when using generics to get typeof(T) in a performant way.

Using Utf8Parser and Utf8Formatter

You can use fast UTF-8-based parsing and formatting methods in your converter logic if your input DateTime or DateTimeOffset text representations are compliant with one of the "R", "l", "O", or "G" standard date and time format strings, or you want to write according to one of these formats. This is much faster than using DateTime(Offset).Parse and DateTime(Offset).ToString.

This example shows a custom converter that serializes and deserializes DateTime values according to the "R" standard format:

using System;
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://docs.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 the following output:
// 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"

Note

The "R" standard format will always be 29 characters long.

Using DateTime(Offset).Parse as a fallback to the serializer's native parsing

If you generally expect your input DateTime or DateTimeOffset data to conform to the extended ISO 8601-1:2019 profile, you can use the serializer's native parsing logic. You can also implement a fallback mechanism just in case. This example shows that, after failing to parse a DateTime text representation using TryGetDateTime(DateTime), the converter successfully parses the data using Parse(String).

using System;
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 the following output:
// 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"

When writing with Utf8JsonWriter

If you want to write a custom DateTime or DateTimeOffset text representation with Utf8JsonWriter, you can format your custom representation to a String, ReadOnlySpan<Byte>, ReadOnlySpan<Char>, or JsonEncodedText, then pass it to the corresponding Utf8JsonWriter.WriteStringValue or Utf8JsonWriter.WriteString method.

The following example shows how a custom DateTime format can be created with ToString(String, IFormatProvider), then written with the WriteStringValue(String) method:

using System;
using System.Globalization;
using System.IO;
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
// }

When reading with Utf8JsonReader

If you want to read a custom DateTime or DateTimeOffset text representation with Utf8JsonReader, you can get the value of the current JSON token as a String using GetString(), then parse the value using custom logic.

The following example shows how a custom DateTimeOffset text representation can be retrieved using GetString(), then parsed using ParseExact(String, String, IFormatProvider):

using System;
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 the following output:
// 7/26/2019 12:00:00 AM -04:00

The extended ISO 8601-1:2019 profile in System.Text.Json

Date and time components

The extended ISO 8601-1:2019 profile implemented in System.Text.Json defines the following components for date and time representations. These components are used to define various levels of granularity supported when parsing and formatting DateTime and DateTimeOffset representations.

Component Format Description
Year "yyyy" 0001-9999
Month "MM" 01-12
Day "dd" 01-28, 01-29, 01-30, 01-31 based on month/year
Hour "HH" 00-23
Minute "mm" 00-59
Second "ss" 00-59
Second fraction "FFFFFFF" Minimum of one digit, maximum of 16 digits
Time offset "K" Either "Z" or "('+'/'-')HH':'mm"
Partial time "HH':'mm':'ss[FFFFFFF]" Time without UTC offset information
Full date "yyyy'-'MM'-'dd" Calendar date
Full time "'Partial time'K" UTC of day or Local time of day with the time offset between local time and UTC
Date time "'Full date''T''Full time'" Calendar date and time of day, e.g. 2019-07-26T16:59:57-05:00

Support for parsing

The following levels of granularity are defined for parsing:

  1. 'Full date'

    1. "yyyy'-'MM'-'dd"
  2. "'Full date''T''Hour'':''Minute'"

    1. "yyyy'-'MM'-'dd'T'HH':'mm"
  3. "'Full date''T''Partial time'"

    1. "yyyy'-'MM'-'dd'T'HH':'mm':'ss" (The Sortable ("s") Format Specifier)
    2. "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'FFFFFFF"
  4. "'Full date''T''Time hour'':''Minute''Time offset'"

    1. "yyyy'-'MM'-'dd'T'HH':'mmZ"
    2. "yyyy'-'MM'-'dd'T'HH':'mm('+'/'-')HH':'mm"
  5. 'Date time'

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

If there are decimal fractions for seconds, there must be at least one digit; 2019-07-26T00:00:00. is not allowed. While up to 16 fractional digits are allowed, only the first seven are parsed. Anything beyond that is considered a zero. For example, 2019-07-26T00:00:00.1234567890 will be parsed as if it is 2019-07-26T00:00:00.1234567. This is to stay compatible with the DateTime implementation, which is limited to this resolution.

Leap seconds are not supported.

Support for formatting

The following levels of granularity are defined for formatting:

  1. "'Full date''T''Partial time'"

    1. "yyyy'-'MM'-'dd'T'HH':'mm':'ss" (The Sortable ("s") Format Specifier)

      Used to format a DateTime without fractional seconds and without offset information.

    2. "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'FFFFFFF"

      Used to format a DateTime with fractional seconds but without offset information.

  2. 'Date time'

    1. "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"

      Used to format a DateTime or DateTimeOffset without fractional seconds but with a UTC offset.

    2. "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'FFFFFFFZ"

      Used to format a DateTime or DateTimeOffset with fractional seconds and with a UTC offset.

    3. "yyyy'-'MM'-'dd'T'HH':'mm':'ss('+'/'-')HH':'mm"

      Used to format a DateTime or DateTimeOffset without fractional seconds but with a local offset.

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

      Used to format a DateTime or DateTimeOffset with fractional seconds and with a local offset.

If present, a maximum of 7 fractional digits are written. This aligns with the DateTime implementation, which is limited to this resolution.