-
Couldn't load subscription status.
- Fork 5.2k
Add DateOnly and TimeOnly support to System.Text.Json #69160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1f1f413
6b196ea
9fbaf0c
0e3a201
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // ------------------------------------------------------------------------------ | ||
| // Changes to this file must follow the https://aka.ms/api-review process. | ||
| // ------------------------------------------------------------------------------ | ||
|
|
||
| namespace System.Text.Json.Serialization.Metadata | ||
| { | ||
| public static partial class JsonMetadataServices | ||
| { | ||
| public static System.Text.Json.Serialization.JsonConverter<System.DateOnly> DateOnlyConverter { get { throw null; } } | ||
| public static System.Text.Json.Serialization.JsonConverter<System.TimeOnly> TimeOnlyConverter { get { throw null; } } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ private struct DateTimeParseData | |
| public int Year; | ||
| public int Month; | ||
| public int Day; | ||
| public bool IsCalendarDateOnly; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know if it would help with #69447 (comment), but defining this field here will increase the size of this struct from 40 bytes to 48 bytes on 64-bit. If you instead move this field to the end, the size should stay at 40. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not seeing a difference in benchmarks when I rearrange the fields, but I could try pushing the change and see if it registers in the performance infrastructure. |
||
| public int Hour; | ||
| public int Minute; | ||
| public int Second; | ||
|
|
@@ -23,94 +24,6 @@ private struct DateTimeParseData | |
| public byte OffsetToken; | ||
| } | ||
|
|
||
| public static string FormatDateTimeOffset(DateTimeOffset value) | ||
| { | ||
| Span<byte> span = stackalloc byte[JsonConstants.MaximumFormatDateTimeOffsetLength]; | ||
|
|
||
| JsonWriterHelper.WriteDateTimeOffsetTrimmed(span, value, out int bytesWritten); | ||
|
|
||
| return JsonReaderHelper.GetTextFromUtf8(span.Slice(0, bytesWritten)); | ||
| } | ||
|
|
||
| public static string FormatDateTime(DateTime value) | ||
| { | ||
| Span<byte> span = stackalloc byte[JsonConstants.MaximumFormatDateTimeOffsetLength]; | ||
|
|
||
| JsonWriterHelper.WriteDateTimeTrimmed(span, value, out int bytesWritten); | ||
|
|
||
| return JsonReaderHelper.GetTextFromUtf8(span.Slice(0, bytesWritten)); | ||
| } | ||
|
|
||
| public static bool TryParseAsISO(ReadOnlySpan<char> source, out DateTime value) | ||
| { | ||
| if (!IsValidDateTimeOffsetParseLength(source.Length)) | ||
| { | ||
| value = default; | ||
| return false; | ||
| } | ||
|
|
||
| int maxLength = checked(source.Length * JsonConstants.MaxExpansionFactorWhileTranscoding); | ||
|
|
||
| Span<byte> bytes = maxLength <= JsonConstants.StackallocByteThreshold | ||
| ? stackalloc byte[JsonConstants.StackallocByteThreshold] | ||
| : new byte[maxLength]; | ||
|
|
||
| int length = JsonReaderHelper.GetUtf8FromText(source, bytes); | ||
|
|
||
| bytes = bytes.Slice(0, length); | ||
|
|
||
| if (bytes.IndexOf(JsonConstants.BackSlash) != -1) | ||
| { | ||
| return JsonReaderHelper.TryGetEscapedDateTime(bytes, out value); | ||
| } | ||
|
|
||
| Debug.Assert(bytes.IndexOf(JsonConstants.BackSlash) == -1); | ||
|
|
||
| if (TryParseAsISO(bytes, out DateTime tmp)) | ||
| { | ||
| value = tmp; | ||
| return true; | ||
| } | ||
|
|
||
| value = default; | ||
| return false; | ||
| } | ||
|
|
||
| public static bool TryParseAsISO(ReadOnlySpan<char> source, out DateTimeOffset value) | ||
| { | ||
| if (!IsValidDateTimeOffsetParseLength(source.Length)) | ||
| { | ||
| value = default; | ||
| return false; | ||
| } | ||
|
|
||
| int maxLength = checked(source.Length * JsonConstants.MaxExpansionFactorWhileTranscoding); | ||
|
|
||
| Span<byte> bytes = maxLength <= JsonConstants.StackallocByteThreshold | ||
| ? stackalloc byte[JsonConstants.StackallocByteThreshold] | ||
| : new byte[maxLength]; | ||
|
|
||
| int length = JsonReaderHelper.GetUtf8FromText(source, bytes); | ||
|
|
||
| bytes = bytes.Slice(0, length); | ||
|
|
||
| if (bytes.IndexOf(JsonConstants.BackSlash) != -1) | ||
| { | ||
| return JsonReaderHelper.TryGetEscapedDateTimeOffset(bytes, out value); | ||
| } | ||
|
|
||
| Debug.Assert(bytes.IndexOf(JsonConstants.BackSlash) == -1); | ||
|
|
||
| if (TryParseAsISO(bytes, out DateTimeOffset tmp)) | ||
| { | ||
| value = tmp; | ||
| return true; | ||
| } | ||
|
|
||
| value = default; | ||
| return false; | ||
| } | ||
|
|
||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static bool IsValidDateTimeOffsetParseLength(int length) | ||
| { | ||
|
|
@@ -180,6 +93,22 @@ public static bool TryParseAsISO(ReadOnlySpan<byte> source, out DateTimeOffset v | |
| return TryCreateDateTimeOffsetInterpretingDataAsLocalTime(parseData, out value); | ||
| } | ||
|
|
||
| #if NETCOREAPP | ||
| public static bool TryParseAsIso(ReadOnlySpan<byte> source, out DateOnly value) | ||
| { | ||
| if (TryParseDateTimeOffset(source, out DateTimeParseData parseData) && | ||
| parseData.IsCalendarDateOnly && | ||
| TryCreateDateTime(parseData, DateTimeKind.Unspecified, out DateTime dateTime)) | ||
| { | ||
| value = DateOnly.FromDateTime(dateTime); | ||
| return true; | ||
| } | ||
|
|
||
| value = default; | ||
| return false; | ||
| } | ||
| #endif | ||
|
|
||
| /// <summary> | ||
| /// ISO 8601 date time parser (ISO 8601-1:2019). | ||
| /// </summary> | ||
|
|
@@ -251,7 +180,7 @@ private static bool TryParseDateTimeOffset(ReadOnlySpan<byte> source, out DateTi | |
| // We now have YYYY-MM-DD [dateX] | ||
| if (source.Length == 10) | ||
| { | ||
| // Just a calendar date | ||
| parseData.IsCalendarDateOnly = true; | ||
| return true; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Buffers; | ||
| using System.Diagnostics; | ||
| using System.Globalization; | ||
|
|
||
| namespace System.Text.Json.Serialization.Converters | ||
| { | ||
| internal sealed class DateOnlyConverter : JsonConverter<DateOnly> | ||
eiriktsarpalis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| public const int FormatLength = 10; // YYYY-MM-DD | ||
| public const int MaxEscapedFormatLength = FormatLength * JsonConstants.MaxExpansionFactorWhileEscaping; | ||
|
|
||
| public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
| { | ||
| if (reader.TokenType != JsonTokenType.String) | ||
| { | ||
| ThrowHelper.ThrowInvalidOperationException_ExpectedString(reader.TokenType); | ||
| } | ||
|
|
||
| return ReadCore(ref reader); | ||
| } | ||
|
|
||
| internal override DateOnly ReadAsPropertyNameCore(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
| { | ||
| return ReadCore(ref reader); | ||
| } | ||
|
|
||
| private DateOnly ReadCore(ref Utf8JsonReader reader) | ||
| { | ||
| bool isEscaped = reader._stringHasEscaping; | ||
| ReadOnlySpan<byte> source = stackalloc byte[0]; | ||
|
|
||
| if (reader.HasValueSequence) | ||
| { | ||
| ReadOnlySequence<byte> valueSequence = reader.ValueSequence; | ||
| long sequenceLength = valueSequence.Length; | ||
|
|
||
| if (!JsonHelpers.IsInRangeInclusive(sequenceLength, FormatLength, MaxEscapedFormatLength)) | ||
| { | ||
| ThrowHelper.ThrowFormatException(DataType.DateOnly); | ||
| } | ||
|
|
||
| Span<byte> stackSpan = stackalloc byte[isEscaped ? FormatLength : MaxEscapedFormatLength]; | ||
| valueSequence.CopyTo(stackSpan); | ||
| source = stackSpan.Slice(0, (int)sequenceLength); | ||
| } | ||
| else | ||
| { | ||
| source = reader.ValueSpan; | ||
|
|
||
| if (!JsonHelpers.IsInRangeInclusive(source.Length, FormatLength, MaxEscapedFormatLength)) | ||
| { | ||
| ThrowHelper.ThrowFormatException(DataType.DateOnly); | ||
| } | ||
| } | ||
|
|
||
| if (isEscaped) | ||
| { | ||
| int backslash = source.IndexOf(JsonConstants.BackSlash); | ||
| Debug.Assert(backslash != -1); | ||
|
|
||
| Span<byte> sourceUnescaped = stackalloc byte[MaxEscapedFormatLength]; | ||
|
|
||
| JsonReaderHelper.Unescape(source, sourceUnescaped, backslash, out int written); | ||
| Debug.Assert(written > 0); | ||
|
|
||
| source = sourceUnescaped.Slice(0, written); | ||
| Debug.Assert(!source.IsEmpty); | ||
| } | ||
|
|
||
| if (!JsonHelpers.TryParseAsIso(source, out DateOnly value)) | ||
| { | ||
| ThrowHelper.ThrowFormatException(DataType.DateOnly); | ||
| } | ||
|
|
||
| return value; | ||
| } | ||
|
|
||
| public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) | ||
| { | ||
| Span<char> buffer = stackalloc char[FormatLength]; | ||
| bool formattedSuccessfully = value.TryFormat(buffer, out int charsWritten, "O", CultureInfo.InvariantCulture); | ||
| Debug.Assert(formattedSuccessfully && charsWritten == FormatLength); | ||
| writer.WriteStringValue(buffer); | ||
| } | ||
|
|
||
| internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options, bool isWritingExtensionDataProperty) | ||
eiriktsarpalis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| Span<char> buffer = stackalloc char[FormatLength]; | ||
| bool formattedSuccessfully = value.TryFormat(buffer, out int charsWritten, "O", CultureInfo.InvariantCulture); | ||
| Debug.Assert(formattedSuccessfully && charsWritten == FormatLength); | ||
| writer.WritePropertyName(buffer); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Buffers.Text; | ||
| using System.Diagnostics; | ||
| using System.Globalization; | ||
|
|
||
| namespace System.Text.Json.Serialization.Converters | ||
| { | ||
| internal sealed class TimeOnlyConverter : JsonConverter<TimeOnly> | ||
eiriktsarpalis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| private static readonly TimeSpanConverter s_timeSpanConverter = new TimeSpanConverter(); | ||
| private static readonly TimeSpan s_timeOnlyMaxValue = TimeOnly.MaxValue.ToTimeSpan(); | ||
|
|
||
| public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
| { | ||
| TimeSpan timespan = s_timeSpanConverter.Read(ref reader, typeToConvert, options); | ||
eiriktsarpalis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if (timespan < TimeSpan.Zero || timespan > s_timeOnlyMaxValue) | ||
| { | ||
| ThrowHelper.ThrowJsonException(); | ||
| } | ||
|
|
||
| return TimeOnly.FromTimeSpan(timespan); | ||
| } | ||
|
|
||
| public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) | ||
| { | ||
| s_timeSpanConverter.Write(writer, value.ToTimeSpan(), options); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.