You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
@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.
Reacted by Lavkesh Dwivedi, Jins Peter, Tomislav Markovski, Kyrylo Yavorenko, MadhuN, Thibaut Dambacher, Augusto Barreto, Anders Dalvander, crispywong, Erik Morén and 25 more
Reacted by peef, Erik Morén, puscasu-ion-daniel, killerwife, Eirik Tsarpalis, Buzurg Arjmandi, Nikolai Koudelia, Nikolay Vasilyev, David Henley, Anas Boumali and 20 more
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. :(
Reacted by Augusto Barreto, Robert, Dave Manton, Vadim Ovchinnikov, Andrea Cassioli, Daniel West, Danilo Breda, Nguyên Phạm, Johan Kvint, cheema-corellian and 33 more
@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.
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.
Reacted by Balint Szabo, Adnan Kamili, Eirik H, VanDenHendeSimon and Titouan CREACH
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?
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.
Reacted by Oskar Klintrot, Eirik H, Dave Black, paulroho-orf and Neven Marić
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):
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...
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:
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.
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.
@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.
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:00Utc
12:00+00:00
"2024-06-01T12:00:00-05:00"
19:00Local (machine zone)
12:00-05:00
"2024-06-01T12:00:00"
12:00Unspecified
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):
Reading ...Z yields 14:00Local; 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.
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.
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.varoptions=newJsonSerializerOptions{DateTimeZoneHandling=JsonDateTimeZoneHandling.Utc};varreading=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(newReading{TakenAt=fromDatabase},options);// {"takenAt":"2024-06-01T12:00:00Z"}
Produced by running the prototype, not by description:
RoundtripKind (default)
Utc
Local
read ...Z
12:00Utc
12:00Utc
14:00Local
read ...-05:00
19:00Local
17:00Utc
19:00Local
read ... (no zone)
12:00Unspecified
12:00Utc
12:00Local
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.
DateTimeOffsetwriting 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.
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.
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
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.
Do JsonSerializerDefaults.Web and .Strict stay on RoundtripKind? Tentatively yes; Strict could reasonably adopt Utc later, but that is a separate discussion.
Per-member attribute now or later? Tentatively later, as argued above.
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.
Activity
steveharter commentedon Oct 1, 2019
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.
layomia commentedon Nov 8, 2019
@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.Localis created. "Z" will giveDateTimeKind.Utc. If no offset is given, then a DateTime withDateTimeKind.Unspecifiedis created.The reverse mapping applies to serialization: writing a
DateTimewithDateTimeKind.Localwill 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.
[-]System.Text.Json default date handling[/-][+]System.Text.Json default DateTimeZoneHandling[/+]mpashkovskiy commentedon May 31, 2020
@btecu unfortunately the only way, I think, to have the similar option in
System.Text.Jsonis to do:and implement DateTimeConverter like that
dalle commentedon Dec 15, 2020
This extends @mpashkovskiy solution and relies on the functionality in
Utf8JsonReader/Utf8JsonWriterfor actual parsing/formatting.danilobreda commentedon Feb 2, 2021
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. :(
amay5027 commentedon May 6, 2021
Just adding to @mpashkovskiy solution, you could use the SpecifyKind method in .Net to do this as follows:
maulik-modi commentedon Jun 18, 2021
@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.
ysbakker commentedon Jun 25, 2021
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.maulik-modi commentedon Jun 25, 2021
@ysbakker , javascript is not sending string ending with "Z"?
13 remaining items
AndiRudi commentedon Apr 13, 2022
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?eithe commentedon Sep 7, 2022
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:eiriktsarpalis commentedon Sep 7, 2022
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.
OskarKlintrot commentedon Sep 7, 2022
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):
eithe commentedon Sep 7, 2022
Thanks @eiriktsarpalis and @OskarKlintrot, will edit.
HarelM commentedon Jan 29, 2023
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:46for 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...
Astral100 commentedon May 9, 2024
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
DateTimeOffsetas 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_78450521And then you could use
.DateTimeon theDateTimeOffsetproperty.jgador commentedon Oct 12, 2024
Hi @dalle
Thank you for the solution above! It's almost there, but I noticed one subtle issue. When calling
reader.GetDateTime(), theDateTimeKindis set toUnspecifiedif 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 theDateTimeKindis not provided, we assume the value is already in UTC.Here's my proposed solution to handle this correctly:
jeffhandley commentedon Aug 17, 2026
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
Zsuffix and no offset. Those deserialize toDateTimeKind.Unspecified, and there is currently no way to tell the serializer how to interpret them -- so the team needs an options-level setting to forceUnspecifiedto be treated as UTC or Local on both read and write.jeffhandley commentedon Aug 17, 2026
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.
GioAbq commentedon Sep 18, 2026
@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:
2024-06-01T12:00:00, the values are known to be UTC, and there is no way to tell the serializer that. They deserialize asDateTimeKind.Unspecifiedand every laterToUniversalTime()in the application silently shifts them by the server's offset.DateTimeZoneHandling = Utcguaranteed aZon the wire whateverKindthe data layer produced.DateTimevalues arriving from an ORM or a SQLdatetimecolumn areUnspecified, 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()andJsonNode.GetValue<DateTime>()all agree:DateTimeDateTimeOffset"2024-06-01T12:00:00Z"12:00Utc12:00+00:00"2024-06-01T12:00:00-05:00"19:00Local(machine zone)12:00-05:00"2024-06-01T12:00:00"12:00Unspecified12:00+02:00(machine offset)DateTimevalueKind=Utc2024-06-01T12:00:00ZKind=Local2024-06-01T12:00:00+02:00Kind=Unspecified2024-06-01T12:00:00The last row of the first table is why "use
DateTimeOffsetinstead" 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 everyDateTimeZoneHandlingmode.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):DateTime.Parse+ToUniversalTime().ToString("...ss'Z'")...Zyields14:00Local; writing anUnspecifiedvalue shifts it to10:00Z; the format drops the fractional second, so12:00:00.123Zgoes out as12:00:00Z.reader.GetDateTime().ToUniversalTime()12:00becomes10:00Z. Writing anUnspecifiedvalue shifts it too.DateTime.Parse+SpecifyKind...Zyields14:00Local; writing aLocalvalue publishes12:00:00Zfor an instant that is10:00Z.SpecifyKindunless already UTC12:00-05:00becomes19:00Zinstead of17: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
ReadAsPropertyName/WriteAsPropertyNamesilently falls back to the built-in converter forDictionary<DateTime, T>keys, so the normalization does not happen. When it does override them,Utf8JsonReader.GetDateTime()throws on aPropertyNametoken andUtf8JsonWriter.WritePropertyName(DateTime)isinternal, so the key has to be materialized as a string, re-parsed withDateTime.Parseand re-formatted by hand to match the trimmed ISO 8601 shape. That hand-written key then goes throughWritePropertyName(string)and the configured encoder, which escapes it: the built-in writer emits2024-06-01T12:00:00+02:00while a converter emits2024-06-01T12:00:00+02:00for the same value.JsonNode.JsonValue.Create(dateTime).WriteTo(writer, options)writes through the converter captured when the node was created, sooptions.Convertersnever reaches it. An option read by the built-in converter would.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 aDateTime.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. ForKind=Localvalues every implementation pays it. Raw output, both job sizes: docs/benchmark.md.API Proposal
The
Jsonprefix on the enum is deliberate:Newtonsoft.Json.DateTimeZoneHandlingalready 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
Semantics
Produced by running the prototype, not by description:
RoundtripKind(default)UtcLocal...Z12:00Utc12:00Utc14:00Local...-05:0019:00Local17:00Utc19:00Local...(no zone)12:00Unspecified12:00Utc12:00LocalKind=Utc...12:00:00Z...12:00:00Z...14:00:00+02:00Kind=Local...12:00:00+02:00...10:00:00Z...12:00:00+02:00Kind=Unspecified...12:00:00...12:00:00Z...12:00:00+02:00This matches Newtonsoft.Json 13.0.4 cell for cell in the
Utc,LocalandRoundtripKindmodes; the prototype's parity tests assert that against the real package rather than against a description of it.Scope:
DateTime?andDictionary<DateTime, T>keys follow the values.DateTimeOffsetwriting never changes; only offset-less text on read is affected (see Open questions).DateOnly/TimeOnlyhave no zone component and are untouched.Utf8JsonReader/Utf8JsonWriter,JsonDocument/JsonElementand values parsed intoJsonNodeare untouched, since they take noJsonSerializerOptions- the same boundaryNumberHandlingalready has.JsonSerializerDefaults.Weband.StrictkeepRoundtripKind.Alternative Designs
SYSLIB1220as 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 andJsonNodegaps listed above.JsonStringEnumConverter<T>had to go generic for the same reason, and source generation still neededJsonSourceGenerationOptions.UseStringEnumConverteron 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=Localvalues still go out with an offset and offset-bearing input still lands as machine-local time.Unspecifiedmember. Measured, that mode converts an offset to the machine's local time and then dropsKind, 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.[JsonDateTimeZoneHandling]attribute, mirroringJsonNumberHandling. 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
RoundtripKind = 0is today's behavior, so a defaultJsonSerializerOptionsis unchanged. The addition is binary and source additive;System.Text.Jsonalso ships netstandard2.0 and net462, whereTimeZoneInfois available, so the surface stays a superset.Localinherits the hazards of machine-local time. In the Europe/Warsaw gap (2024-03-31, 02:00 -> 03:00), an offset-less02:30is assumed local, gets the daylight offset and comes back as01:30- an hour before the value that went in. In the ambiguous hour (2024-10-27)02:30resolves to the standard-time reading,01:30Z. This is inherent toDateTime+ local time, is what Json.NET does today, and is an argument for documentingLocalas the compatibility mode rather than the recommended one.DateTime.MinValue/MaxValuesaturates, asToUniversalTime()/ToLocalTime()already do. Note thatLocaloutput 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.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
DateTimeOffset? The prototype says yes (Utcgives+00:00instead 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 touchDateTimeOffsetat all.JsonSerializerDefaults.Weband.Strictstay onRoundtripKind? Tentatively yes;Strictcould reasonably adoptUtclater, but that is a separate discussion.DateTimeZoneHandlingorDateTimeKindHandling? 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>andJsonConverter<DateTimeOffset>implementations turns up only test converters (for exampleReadScenarioTests.MyDateTimeConverter), so there are no adoption sites in product code. Outside the repo the pattern is visible - OpenTelemetry's OneCollector exporter andMicrosoft.IdentityModel.Tokensboth callToUniversalTime()beforeWriteStringValue- but both write throughUtf8JsonWriterdirectly, 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
SYSLIB1220result 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 itsdocs/current-behavior.md). Happy to move it to a branch on a fork of dotnet/runtime, withGenerateReferenceAssemblySourceoutput and the tests moved intoSystem.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.