-
Notifications
You must be signed in to change notification settings - Fork 5.3k
System.Text.Json: Add TimeSpanConverter #54186
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 5 commits
9fa6f46
03b10a3
9c5c1f6
bce438f
0293935
cf8d104
6041e5c
2838c26
0248a15
f875e6f
f3e49ff
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,81 @@ | ||
| // 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.Buffers.Text; | ||
| using System.Diagnostics; | ||
|
|
||
| namespace System.Text.Json.Serialization.Converters | ||
| { | ||
| internal sealed class TimeSpanConverter : JsonConverter<TimeSpan> | ||
| { | ||
| private const int MinimumTimeSpanFormatLength = 7; // h:mm:ss | ||
| private const int MaximumTimeSpanFormatLength = 26; // -dddddddd.hh:mm:ss.fffffff | ||
|
|
||
| public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
| { | ||
| if (reader.TokenType != JsonTokenType.String) | ||
| { | ||
| throw ThrowHelper.GetInvalidOperationException_ExpectedString(reader.TokenType); | ||
| } | ||
|
|
||
| ReadOnlySpan<byte> source = stackalloc byte[0]; | ||
|
|
||
| if (reader.HasValueSequence) | ||
| { | ||
| ReadOnlySequence<byte> valueSequence = reader.ValueSequence; | ||
| long sequenceLength = valueSequence.Length; | ||
|
|
||
| if (!JsonHelpers.IsInRangeInclusive(sequenceLength, MinimumTimeSpanFormatLength, MaximumTimeSpanFormatLength)) | ||
| { | ||
| throw ThrowHelper.GetFormatException(); | ||
| } | ||
|
|
||
| Span<byte> stackSpan = stackalloc byte[(int)sequenceLength]; | ||
|
|
||
| valueSequence.CopyTo(stackSpan); | ||
| source = stackSpan; | ||
| } | ||
| else | ||
| { | ||
| source = reader.ValueSpan; | ||
|
|
||
| if (!JsonHelpers.IsInRangeInclusive(source.Length, MinimumTimeSpanFormatLength, MaximumTimeSpanFormatLength)) | ||
| { | ||
| throw ThrowHelper.GetFormatException(); | ||
| } | ||
| } | ||
|
|
||
| bool result = Utf8Parser.TryParse(source, out TimeSpan tmpValue, out int bytesConsumed, 'c'); | ||
|
|
||
| // Note: Utf8Parser.TryParse will return true for invalid input so | ||
| // long as it starts with an integer. Example: "2021-06-18" or | ||
| // "1$$$$$$$$$$". We need to check bytesConsumed to know if the | ||
| // entire source was actually valid. | ||
|
|
||
| if (result && source.Length == bytesConsumed) | ||
| { | ||
| return tmpValue; | ||
| } | ||
|
|
||
| result = Utf8Parser.TryParse(source, out tmpValue, out bytesConsumed, 'g'); | ||
layomia marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if (result && source.Length == bytesConsumed) | ||
| { | ||
| return tmpValue; | ||
| } | ||
|
|
||
| throw ThrowHelper.GetFormatException(); | ||
| } | ||
|
|
||
| public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) | ||
| { | ||
| Span<byte> output = stackalloc byte[MaximumTimeSpanFormatLength]; | ||
|
|
||
| bool result = Utf8Formatter.TryFormat(value, output, out int bytesWritten, 'c'); | ||
| Debug.Assert(result); | ||
|
|
||
| writer.WriteStringValue(output.Slice(0, bytesWritten)); | ||
|
Contributor
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. [Non-blocking for this PR] When #54254 goes in, we should consider updating this to use the new |
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
|
|
||
| using System.Globalization; | ||
| using System.Runtime.CompilerServices; | ||
| using Newtonsoft.Json; | ||
| using Xunit; | ||
|
|
||
| namespace System.Text.Json.Serialization.Tests | ||
|
|
@@ -80,6 +81,7 @@ public static void ReadPrimitivesFail() | |
|
|
||
| Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<DateTime>("\"abc\"")); | ||
| Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<DateTimeOffset>("\"abc\"")); | ||
| Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<TimeSpan>("\"abc\"")); | ||
| Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<Guid>("\"abc\"")); | ||
|
|
||
| Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<byte>("\"abc\"")); | ||
|
|
@@ -118,6 +120,7 @@ public static void ReadPrimitivesFail() | |
| [InlineData(typeof(sbyte))] | ||
| [InlineData(typeof(float))] | ||
| [InlineData(typeof(string))] | ||
| [InlineData(typeof(TimeSpan))] | ||
| [InlineData(typeof(ushort))] | ||
| [InlineData(typeof(uint))] | ||
| [InlineData(typeof(ulong))] | ||
|
|
@@ -303,6 +306,9 @@ public static void ValueFail() | |
| Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<DateTimeOffset>(unexpectedString)); | ||
| Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<DateTimeOffset?>(unexpectedString)); | ||
|
|
||
| Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<TimeSpan>(unexpectedString)); | ||
| Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<TimeSpan?>(unexpectedString)); | ||
|
Contributor
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. Due to other logic, we should be fine, but a few more nullable |
||
|
|
||
| Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<string>("1")); | ||
|
|
||
| Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<char>("1")); | ||
|
|
@@ -442,5 +448,64 @@ private static void DeserializeLongJsonString(int stringLength) | |
| string str = JsonSerializer.Deserialize<string>(json); | ||
| Assert.True(json.AsSpan(1, json.Length - 2).SequenceEqual(str.AsSpan())); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("23:59:59")] | ||
| [InlineData("23:59:59.9", "23:59:59.9000000")] | ||
| [InlineData("23:59:59.9999999")] | ||
| [InlineData("1:00:00", "01:00:00")] // 'g' Format | ||
| [InlineData("1:2:00:00", "1.02:00:00")] // 'g' Format | ||
| [InlineData("9999999.23:59:59.9999999")] | ||
| [InlineData("-9999999.23:59:59.9999999")] | ||
CodeBlanch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| [InlineData("10675199.02:48:05.4775807")] // TimeSpan.MaxValue | ||
| [InlineData("-10675199.02:48:05.4775808")] // TimeSpan.MinValue | ||
| public static void TimeSpan_Read_Success(string json, string? actual = null) | ||
CodeBlanch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| TimeSpan value = JsonSerializer.Deserialize<TimeSpan>($"\"{json}\""); | ||
|
|
||
| Assert.Equal(TimeSpan.Parse(actual ?? json), value); | ||
| Assert.Equal(value, JsonConvert.DeserializeObject<TimeSpan>($"\"{json}\"")); | ||
| } | ||
|
|
||
| [Fact] | ||
| public static void TimeSpan_Read_KnownDifferences() | ||
| { | ||
| string value = "24:00:00"; | ||
|
|
||
| // 24:00:00 should be invalid because hours can only be up to 23. | ||
| Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<TimeSpan>($"\"{value}\"")); | ||
|
|
||
| TimeSpan expectedValue = TimeSpan.Parse("24.00:00:00"); | ||
|
|
||
| // TimeSpan.Parse has a quirk where it treats 24:00:00 as 24.00:00:00. | ||
| Assert.Equal(expectedValue, TimeSpan.Parse(value)); | ||
|
|
||
| // Newtonsoft uses TimeSpan.Parse so it is subject to the quirk. | ||
| Assert.Equal(expectedValue, JsonConvert.DeserializeObject<TimeSpan>($"\"{value}\"")); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("24:00:00")] | ||
| [InlineData("00:60:00")] | ||
| [InlineData("00:00:60")] | ||
| [InlineData("00:00:00.00000009")] | ||
| [InlineData("900000000.00:00:00")] | ||
| [InlineData("+00:00:00")] | ||
| [InlineData("2021-06-18")] | ||
| [InlineData("1$")] | ||
CodeBlanch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| [InlineData("10675199.02:48:05.4775808")] // TimeSpan.MaxValue + 1 | ||
| [InlineData("-10675199.02:48:05.4775809")] // TimeSpan.MinValue - 1 | ||
| [InlineData("1234", false)] | ||
| [InlineData("{}", false)] | ||
| [InlineData("[]", false)] | ||
| [InlineData("true", false)] | ||
| [InlineData("null", false)] | ||
| public static void TimeSpan_Read_Failure(string json, bool addQuotes = true) | ||
| { | ||
| if (addQuotes) | ||
| json = $"\"{json}\""; | ||
|
|
||
| Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<TimeSpan>(json)); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.