Skip to content

System.Text.Json default DateTimeZoneHandling #1566

Open

Description

@btecu

In JSON.NET you can:

JsonConvert.DefaultSettings = () => new JsonSerializerSettings {
    DateTimeZoneHandling = DateTimeZoneHandling.Utc
};

Is there anything similar in System.Text.Json?
If there is not an option to set the default, can it be passed in manually?

Also, what is the default (Local, Utc, Unspecified)?

Activity

  1. steveharter commented on Oct 1, 2019

    @steveharter
    Contributor

    cc @layomia on dates

    Since a DateTime can be set in each of those formats (e.g. with and without the timezone\offset), the reader\writer (and thus the serializer) uses what is available and uses\assumes an ISO 8601 format to represent that.

  2. layomia commented on Nov 8, 2019

    @layomia
    Contributor

    @btecu the ISO 8601 profile is used by default in the serializer, e.g. 2019-07-26T16:59:57-05:00.

    For deserializing, if the datetime offset is given as "hh:mm", a DateTime with DateTimeKind.Local is created. "Z" will give DateTimeKind.Utc. If no offset is given, then a DateTime with DateTimeKind.Unspecified is created.
    The reverse mapping applies to serialization: writing a DateTime with DateTimeKind.Local will yield an ISO representation with an "hh:mm"-formatted offset, and so on.

    For more on DateTime and DateTimeOffset support in System.Text.Json, and how you can implement custom parsing or formatting, see https://docs.microsoft.com/en-us/dotnet/standard/datetime/system-text-json-support.

  3. changed the title [-]System.Text.Json default date handling[/-] [+]System.Text.Json default DateTimeZoneHandling[/+] on Nov 23, 2019
  4. transferred this issue fromdotnet/corefxon Jan 9, 2020
  5. removed
    untriagedNew issue has not been triaged by the area owner
    on Feb 20, 2020
  6. added this to the Future milestone on Feb 20, 2020
  7. mpashkovskiy commented on May 31, 2020

    @mpashkovskiy

    @btecu unfortunately the only way, I think, to have the similar option in System.Text.Json is to do:

    services.AddControllers()
        .AddJsonOptions(options =>
         {
             options.JsonSerializerOptions.Converters.Add(new DateTimeConverter());
         });

    and implement DateTimeConverter like that

    public class DateTimeConverter : JsonConverter<DateTime>
    {
        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return DateTime.Parse(reader.GetString());
        }
    
        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        {
             writer.WriteStringValue(value.ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"));
        }
    }
  8. dalle commented on Dec 15, 2020

    @dalle

    This extends @mpashkovskiy solution and relies on the functionality in Utf8JsonReader/Utf8JsonWriter for actual parsing/formatting.

    public class DateTimeConverter : JsonConverter<DateTime>
    {
        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return reader.GetDateTime().ToUniversalTime();
        }
    
        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        {
             writer.WriteStringValue(value.ToUniversalTime());
        }
    }
  9. danilobreda commented on Feb 2, 2021

    @danilobreda

    This solution should come natively so that we have the best possible performance. JsonConverter creates problems when poorly implemented.
    Sometimes we can't control which Kind of datetime because of its origin being an ORM for example ... needing a .ToUniversalTime() just to satisfy json deserealization by adding a +00:00 or "Z"
    This solution should be reviewed. :(

  10. amay5027 commented on May 6, 2021

    @amay5027

    Just adding to @mpashkovskiy solution, you could use the SpecifyKind method in .Net to do this as follows:

        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return DateTime.Parse(reader.GetString());
        }
    
        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(DateTime.SpecifyKind(value, DateTimeKind.Utc));
        }
    
  11. maulik-modi commented on Jun 18, 2021

    @maulik-modi

    @davidfowl and @JamesNK , what do you guys recommend here? We are building up new project using System.Text.json, want to ensure we use the right guidance and get benefit from system.text.json.

  12. ysbakker commented on Jun 25, 2021

    @ysbakker

    Just adding to @mpashkovskiy solution, you could use the SpecifyKind method in .Net to do this as follows:

        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return DateTime.Parse(reader.GetString());
        }
    
        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(DateTime.SpecifyKind(value, DateTimeKind.Utc));
        }
    

    Exactly what I was looking for! I save all DateTime as UTC in the database because I have different timezones in development and production. I then parse the UTC time to local time in the client, but by default the json serializer didn't append a "Z" so I had to do some ugly string concatenation to make it work. This solves that issue by specifying that the date is already in UTC and will therefore return a nice ISO string that javascript also understands. The issue with using DateTime.ToUniversalTime() in my case is that it assumes I save the timestamps in my local timezone.

  13. maulik-modi commented on Jun 25, 2021

    @maulik-modi

    @ysbakker , javascript is not sending string ending with "Z"?

  14. 13 remaining items

  15. AndiRudi commented on Apr 13, 2022

    @AndiRudi

    This is a really annoying thing when you want to upgrade to .net 6 because the default de-serialiser of a view model will add the timezone given by the frontend. If you forward this to a Postgres database you will get an error like Cannot write DateTimeOffset with Offset=02:00:00 to PostgreSQL type .... Or am I missing something?

  16. eithe commented on Sep 7, 2022

    @eithe

    Edit: Please do not use this, see replies from @eiriktsarpalis and @OskarKlintrot below.

    For anyone who uses DateTime?, here is a nullable variant of @dalle's solution:

    public class NullableDateTimeConverter : JsonConverter<DateTime?>
    {
        public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return reader.TryGetDateTime(out var value) ? value.ToUniversalTime() : null;
        }
    
        public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
        {
            if (value.HasValue)
            {
                writer.WriteStringValue(value.Value.ToUniversalTime());
            }
            else
            {
                writer.WriteNullValue();
            }
        }
    }
  17. eiriktsarpalis commented on Sep 7, 2022

    @eiriktsarpalis
    Member

    For anyone who uses DateTime?

    Writing dedicated converters for nullable types is not recommended. Authoring a custom converter for the underlying struct should suffice, the generic nullable converter will compose with that.

  18. OskarKlintrot commented on Sep 7, 2022

    @OskarKlintrot

    And you can access the other converters from inside your converter (to avoid doing the convertion yourself if there's already a perfectly good converter at your disposal once you done with type checks or whatever you want to do):

    options.GetConverter(typeof(DateTime))
    
  19. eithe commented on Sep 7, 2022

    @eithe

    Thanks @eiriktsarpalis and @OskarKlintrot, will edit.

  20. HarelM commented on Jan 29, 2023

    @HarelM

    Would be great to have some build it converter in .Net for these kind of scenarios.
    I'm trying to migrate my project from Newtonsoft.JSON to system.text.json and a lot of places are failing, some related to this, some related to other issues.
    I don't want to write a generic datetime converter that can parse a date well.
    I have an external API that is written in python that has a date of the format: 2021-08-30 23:26:46 for example. Previously it just worked.
    When I migrated to STJ this started failing. While I get the performance advantages of assuming everything is ISO, I still think there should be an easy way to say: "hey, converter, this date isn't well formatted, but let's make sure you are able to parse it" without writing a converter that was already written hundreds of times...

  21. Astral100 commented on May 9, 2024

    @Astral100

    If the goal is to get pure local time (by stripping timezone part from the datetime), then one solution I found is to change the contract of the received object properties to DateTimeOffset as described here (in EDIT2 part): https://stackoverflow.com/questions/78450474/how-to-strip-off-timezone-part-from-datetime-to-leave-only-local-datetime-part/78450521?noredirect=1#comment138306952_78450521

    And then you could use .DateTime on the DateTimeOffset property.

  22. jgador commented on Oct 12, 2024

    @jgador

    Hi @dalle

    Thank you for the solution above! It's almost there, but I noticed one subtle issue. When calling reader.GetDateTime(), the DateTimeKind is set to Unspecified if the input doesn't contain time zone information. The code converts the date time to UTC even though the original time zone is unknown. What we want is to treat the time as UTC when time zone information is present. If the DateTimeKind is not provided, we assume the value is already in UTC.

    Here's my proposed solution to handle this correctly:

    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var date = reader.GetDateTime();
    
        return date.Kind == DateTimeKind.Utc
            ? date
            : DateTime.SpecifyKind(date, DateTimeKind.Utc);
    }
  23. jeffhandley commented on Aug 17, 2026

    @jeffhandley
    Member

    Another request for this landed as #122962, now closed as a duplicate. Adding its scenario here since it is a concrete driver: the reporter's application consumes external, non-.NET sites that emit date-time values with no Z suffix and no offset. Those deserialize to DateTimeKind.Unspecified, and there is currently no way to tell the serializer how to interpret them -- so the team needs an options-level setting to force Unspecified to be treated as UTC or Local on both read and write.

  24. added
    help wanted[up-for-grabs] Good issue for external contributors
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    and removed
    wishlistIssue we would like to prioritize, but we can't commit we will get to it yet
    on Aug 17, 2026
  25. jeffhandley commented on Aug 17, 2026

    @jeffhandley
    Member

    With #122962 being closed as a duplicate of this, I marked this as help wanted to invite assistance reviewing the full history of this issue and authoring a revised API proposal that incorporates what was included in #122962 along with other scenarios discussed here.

    Once we have a comprehensive proposal commented here, we can update the issue description and mark it as api-ready-for-review.

  26. GioAbq commented on Sep 18, 2026

    @GioAbq

    @jeffhandley Here is a revised proposal covering this thread and the scenario from #122962.

    Everything below was measured on .NET 10.0.12 (SDK 10.0.401), Windows, with the machine time zone at UTC+02:00. The prototype that produced the "proposed" rows, its 94 tests and the benchmark are linked at the end.

    Background and motivation

    Two concrete drivers, both raised here:

    1. Date-time text without a zone, from producers that are not .NET ([API Proposal]: System.Text.Json: Add JsonSerializerOptions.DateTimeUnspecifiedKindHandling property #122962, @wdnijdam): the payload carries 2024-06-01T12:00:00, the values are known to be UTC, and there is no way to tell the serializer that. They deserialize as DateTimeKind.Unspecified and every later ToUniversalTime() in the application silently shifts them by the server's offset.
    2. Json.NET migrations (@btecu, @xenod, @maulik-modi, @danilobreda, @seekingtheoptimal): DateTimeZoneHandling = Utc guaranteed a Z on the wire whatever Kind the data layer produced. DateTime values arriving from an ORM or a SQL datetime column are Unspecified, so today the same payload goes out without a suffix.

    Current behavior

    JsonSerializer (reflection and source generation), DateTime?, Dictionary<DateTime, T> keys, Utf8JsonReader.GetDateTime(), JsonElement.GetDateTime() and JsonNode.GetValue<DateTime>() all agree:

    JSON DateTime DateTimeOffset
    "2024-06-01T12:00:00Z" 12:00 Utc 12:00+00:00
    "2024-06-01T12:00:00-05:00" 19:00 Local (machine zone) 12:00-05:00
    "2024-06-01T12:00:00" 12:00 Unspecified 12:00+02:00 (machine offset)
    DateTime value written as
    Kind=Utc 2024-06-01T12:00:00Z
    Kind=Local 2024-06-01T12:00:00+02:00
    Kind=Unspecified 2024-06-01T12:00:00

    The last row of the first table is why "use DateTimeOffset instead" does not answer driver 1: offset-less text silently takes the reading machine's offset, which is the bug the reporter is trying to avoid. Newtonsoft.Json 13.0.4 behaves the same way here, in every DateTimeZoneHandling mode.

    The workarounds in this thread do not hold

    Measured with the converters as posted (for @jgador, whose comment supplies only a Read, the write side is @dalle's):

    Converter Measured defect
    @mpashkovskiy DateTime.Parse + ToUniversalTime().ToString("...ss'Z'") Reading ...Z yields 14:00 Local; writing an Unspecified value shifts it to 10:00Z; the format drops the fractional second, so 12:00:00.123Z goes out as 12:00:00Z.
    @dalle reader.GetDateTime().ToUniversalTime() Offset-less input is shifted by the machine offset: 12:00 becomes 10:00Z. Writing an Unspecified value shifts it too.
    @amay5027 DateTime.Parse + SpecifyKind Reading ...Z yields 14:00 Local; writing a Local value publishes 12:00:00Z for an instant that is 10:00Z.
    @jgador SpecifyKind unless already UTC Offset-bearing input yields the wrong instant: 12:00-05:00 becomes 19:00Z instead of 17:00Z.

    The first three are the most upvoted answers in the thread (35, 30 and 11 upvotes); the fourth was posted as a correction to the second, and the second and third both say they extend the first. Each is wrong in a different case. Seven years of people copying them is the strongest argument for putting the policy in the box.

    Three gaps a user-supplied converter cannot close

    • Dictionary keys. A converter that does not override ReadAsPropertyName/WriteAsPropertyName silently falls back to the built-in converter for Dictionary<DateTime, T> keys, so the normalization does not happen. When it does override them, Utf8JsonReader.GetDateTime() throws on a PropertyName token and Utf8JsonWriter.WritePropertyName(DateTime) is internal, so the key has to be materialized as a string, re-parsed with DateTime.Parse and re-formatted by hand to match the trimmed ISO 8601 shape. That hand-written key then goes through WritePropertyName(string) and the configured encoder, which escapes it: the built-in writer emits 2024-06-01T12:00:00+02:00 while a converter emits 2024-06-01T12:00:00+02:00 for the same value.
    • JsonNode. JsonValue.Create(dateTime).WriteTo(writer, options) writes through the converter captured when the node was created, so options.Converters never reaches it. An option read by the built-in converter would.
    • Cost. The converter dispatch is not what costs; the time-zone conversion is. Across three BenchmarkDotNet runs (.NET 10.0.12, one developer machine I could not quiet down - StdDev reaches 25% of the mean), the copied converter consistently costs 1.8-2.3x the built-in path on read and 1.3-2.2x on write, because it calls ToUniversalTime() on every value. The difference between the built-in path and the proposed option landed inside that noise in every run, so I am not putting a number on it. What is structural rather than measured: "this offset-less text is already UTC" is a DateTime.SpecifyKind, not a conversion, so the option needs no zone lookup in the scenario [API Proposal]: System.Text.Json: Add JsonSerializerOptions.DateTimeUnspecifiedKindHandling property #122962 describes, while the workaround pays for one. For Kind=Local values every implementation pays it. Raw output, both job sizes: docs/benchmark.md.

    API Proposal

    namespace System.Text.Json.Serialization;
    
    public enum JsonDateTimeZoneHandling
    {
        RoundtripKind = 0, // today's behavior
        Utc = 1,
        Local = 2,
    }
    
    public partial class JsonSourceGenerationOptionsAttribute
    {
        public JsonDateTimeZoneHandling DateTimeZoneHandling { get; set; }
    }
    
    namespace System.Text.Json;
    
    public partial class JsonSerializerOptions
    {
        // EXISTING
        // public JsonNumberHandling NumberHandling { get; set; }
    
        public JsonDateTimeZoneHandling DateTimeZoneHandling { get; set; }
    }

    The Json prefix on the enum is deliberate: Newtonsoft.Json.DateTimeZoneHandling already exists, and a project mid-migration imports both namespaces. An unprefixed name would be a source breaking change (CS0104) for exactly the audience this feature serves.

    API Usage

    // Driver 1: external services send local-looking text that is really UTC.
    var options = new JsonSerializerOptions { DateTimeZoneHandling = JsonDateTimeZoneHandling.Utc };
    
    var reading = JsonSerializer.Deserialize<Reading>("""{"takenAt":"2024-06-01T12:00:00"}""", options);
    // reading.TakenAt == 2024-06-01T12:00:00, Kind == Utc
    
    // Driver 2: whatever the data layer produced goes out with a Z.
    JsonSerializer.Serialize(new Reading { TakenAt = fromDatabase }, options);
    // {"takenAt":"2024-06-01T12:00:00Z"}
    [JsonSourceGenerationOptions(DateTimeZoneHandling = JsonDateTimeZoneHandling.Utc)]
    [JsonSerializable(typeof(Reading))]
    public partial class ReadingContext : JsonSerializerContext { }

    Semantics

    Produced by running the prototype, not by description:

    RoundtripKind (default) Utc Local
    read ...Z 12:00 Utc 12:00 Utc 14:00 Local
    read ...-05:00 19:00 Local 17:00 Utc 19:00 Local
    read ... (no zone) 12:00 Unspecified 12:00 Utc 12:00 Local
    write Kind=Utc ...12:00:00Z ...12:00:00Z ...14:00:00+02:00
    write Kind=Local ...12:00:00+02:00 ...10:00:00Z ...12:00:00+02:00
    write Kind=Unspecified ...12:00:00 ...12:00:00Z ...12:00:00+02:00

    This matches Newtonsoft.Json 13.0.4 cell for cell in the Utc, Local and RoundtripKind modes; the prototype's parity tests assert that against the real package rather than against a description of it.

    Scope:

    • DateTime? and Dictionary<DateTime, T> keys follow the values.
    • DateTimeOffset writing never changes; only offset-less text on read is affected (see Open questions).
    • DateOnly/TimeOnly have no zone component and are untouched.
    • Utf8JsonReader/Utf8JsonWriter, JsonDocument/JsonElement and values parsed into JsonNode are untouched, since they take no JsonSerializerOptions - the same boundary NumberHandling already has.
    • JsonSerializerDefaults.Web and .Strict keep RoundtripKind.

    Alternative Designs

    • A public parameterized converter instead of an option. It cannot be used declaratively, because the handling has to come in through a constructor argument: the source generator only accepts converter types with an accessible parameterless constructor. I tried it - the build reports SYSLIB1220 as a warning, the app compiles and runs, and the value comes out as "2024-06-01T12:00:00", i.e. the converter was silently dropped. Shipping one parameterless type per mode brings back the key and JsonNode gaps listed above. JsonStringEnumConverter<T> had to go generic for the same reason, and source generation still needed JsonSourceGenerationOptions.UseStringEnumConverter on top.
    • DateTimeUnspecifiedKindHandling { AssumeUtc, AssumeLocal } as proposed in [API Proposal]: System.Text.Json: Add JsonSerializerOptions.DateTimeUnspecifiedKindHandling property #122962. It answers driver 1 but leaves driver 2 untouched: Kind=Local values still go out with an offset and offset-bearing input still lands as machine-local time.
    • Full Json.NET parity, including its Unspecified member. Measured, that mode converts an offset to the machine's local time and then drops Kind, so the stored value depends on the server that read it. Reproducing that seemed worse than omitting it; it can be added later without breaking anyone.
    • A per-member [JsonDateTimeZoneHandling] attribute, mirroring JsonNumberHandling. Nothing in seven years of this thread asks for per-property control - every request is an application-wide policy - so this is left out of v1 and stays additive.

    Risks

    • Behavioral compatibility: RoundtripKind = 0 is today's behavior, so a default JsonSerializerOptions is unchanged. The addition is binary and source additive; System.Text.Json also ships netstandard2.0 and net462, where TimeZoneInfo is available, so the surface stays a superset.
    • Local inherits the hazards of machine-local time. In the Europe/Warsaw gap (2024-03-31, 02:00 -> 03:00), an offset-less 02:30 is assumed local, gets the daylight offset and comes back as 01:30 - an hour before the value that went in. In the ambiguous hour (2024-10-27) 02:30 resolves to the standard-time reading, 01:30Z. This is inherent to DateTime + local time, is what Json.NET does today, and is an argument for documenting Local as the compatibility mode rather than the recommended one.
    • Range edges: converting near DateTime.MinValue/MaxValue saturates, as ToUniversalTime()/ToLocalTime() already do. Note that Local output near the minimum (0001-01-01T00:00:00+02:00) cannot be read back - that is JsonSerializer does not support parsing DateTime value '0001-01-01T00:00:00+01:00' #70547, which this proposal neither fixes nor worsens.
    • Implementation surface: the converter for DateTime/DateTimeOffset, the options field with setter validation, the copy constructor and the caching equality comparer, plus the source-generator parser and emitter for the attribute. The reading path should convert offset-bearing text to UTC directly rather than through machine-local time, so values near the range edges are not clamped on the way.

    Open questions

    1. Should offset-less text follow the option when the target is DateTimeOffset? The prototype says yes (Utc gives +00:00 instead of the machine offset), because that is the same failure the option exists to remove and it cannot be widened later without changing behavior for people who already opted in. Json.NET says no - its setting does not touch DateTimeOffset at all.
    2. Do JsonSerializerDefaults.Web and .Strict stay on RoundtripKind? Tentatively yes; Strict could reasonably adopt Utc later, but that is a separate discussion.
    3. Per-member attribute now or later? Tentatively later, as argued above.
    4. DateTimeZoneHandling or DateTimeKindHandling? The first matches the name people are migrating from and searching for; the second describes what it actually manipulates.

    Usage in dotnet/runtime

    Searching the VMR for JsonConverter<DateTime> and JsonConverter<DateTimeOffset> implementations turns up only test converters (for example ReadScenarioTests.MyDateTimeConverter), so there are no adoption sites in product code. Outside the repo the pattern is visible - OpenTelemetry's OneCollector exporter and Microsoft.IdentityModel.Tokens both call ToUniversalTime() before WriteStringValue - but both write through Utf8JsonWriter directly, which this option does not reach. So: no adoption sites inside dotnet/runtime, which is a signal worth stating rather than hiding.

    Prototype

    The semantics, the parity with Newtonsoft.Json 13.0.4, the workaround defects, the SYSLIB1220 result and the benchmark above all come from a prototype outside the repo: https://github.com/GioAbq/stj-datetimezone-prototype (converters implementing the proposed semantics, 94 tests against Newtonsoft.Json 13.0.4 and the built-in behavior, BenchmarkDotNet; the tables above are its docs/current-behavior.md). Happy to move it to a branch on a fork of dotnet/runtime, with GenerateReferenceAssemblySource output and the tests moved into System.Text.Json.Tests, if the shape above looks worth pursuing.

    Note

    This comment was drafted with AI assistance (Claude Code). The measurements, prototype and test results it cites were produced by running the code described above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-System.Text.Jsonhelp wanted[up-for-grabs] Good issue for external contributors

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions