private readonly Type? _timeSpanType;
private readonly Type? _dateTimeOffsetType;
+ private readonly Type? _dateOnlyType;
+ private readonly Type? _timeOnlyType;
private readonly Type? _byteArrayType;
private readonly Type? _guidType;
private readonly Type? _uriType;
private readonly Type _intPtrType;
private readonly Type _uIntPtrType;
- // Unsupported types that may not resolve
- private readonly Type? _dateOnlyType;
- private readonly Type? _timeOnlyType;
-
// Needed for converter validation
private readonly Type _jsonConverterOfTType;
AddTypeIfNotNull(_knownTypes, _byteArrayType);
AddTypeIfNotNull(_knownTypes, _timeSpanType);
AddTypeIfNotNull(_knownTypes, _dateTimeOffsetType);
+ AddTypeIfNotNull(_knownTypes, _dateOnlyType);
+ AddTypeIfNotNull(_knownTypes, _timeOnlyType);
AddTypeIfNotNull(_knownTypes, _guidType);
AddTypeIfNotNull(_knownTypes, _uriType);
AddTypeIfNotNull(_knownTypes, _versionType);
_knownUnsupportedTypes.Add(_intPtrType);
_knownUnsupportedTypes.Add(_uIntPtrType);
- AddTypeIfNotNull(_knownUnsupportedTypes, _dateOnlyType);
- AddTypeIfNotNull(_knownUnsupportedTypes, _timeOnlyType);
-
static void AddTypeIfNotNull(HashSet<Type> types, Type? type)
{
if (type != null)
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
<Compile Include="System.Text.Json.Typeforwards.netcoreapp.cs" />
+ <Compile Include="System.Text.Json.netcoreapp.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
--- /dev/null
+// 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; } }
+ }
+}
<data name="InvalidComparison" xml:space="preserve">
<value>Cannot compare the value of a token type '{0}' to text.</value>
</data>
- <data name="FormatDateTime" xml:space="preserve">
- <value>The JSON value is not in a supported DateTime format.</value>
- </data>
- <data name="FormatDateTimeOffset" xml:space="preserve">
- <value>The JSON value is not in a supported DateTimeOffset format.</value>
- </data>
- <data name="FormatTimeSpan" xml:space="preserve">
- <value>The JSON value is not in a supported TimeSpan format.</value>
- </data>
- <data name="FormatGuid" xml:space="preserve">
- <value>The JSON value is not in a supported Guid format.</value>
- </data>
- <data name="FormatVersion" xml:space="preserve">
- <value>The JSON value is not in a supported Version format.</value>
+ <data name="UnsupportedFormat" xml:space="preserve">
+ <value>The JSON value is not in a supported {0} format.</value>
</data>
<data name="ExpectedStartOfPropertyOrValueAfterComment" xml:space="preserve">
<value>'{0}' is an invalid start of a property name or value, after a comment.</value>
<data name="DefaultIgnoreConditionInvalid" xml:space="preserve">
<value>The value cannot be 'JsonIgnoreCondition.Always'.</value>
</data>
- <data name="FormatBoolean" xml:space="preserve">
- <value>The JSON value is not in a supported Boolean format.</value>
- </data>
<data name="DictionaryKeyTypeNotSupported" xml:space="preserve">
<value>The type '{0}' is not a supported dictionary key using converter of type '{1}'.</value>
</data>
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum)</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
<Compile Include="System.Text.Json.Typeforwards.netcoreapp.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializerOptionsUpdateHandler.cs" />
+ <Compile Include="System\Text\Json\Serialization\Converters\Value\DateOnlyConverter.cs" />
+ <Compile Include="System\Text\Json\Serialization\Converters\Value\TimeOnlyConverter.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
public int Year;
public int Month;
public int Day;
+ public bool IsCalendarDateOnly;
public int Hour;
public int Minute;
public int Second;
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)
{
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>
// We now have YYYY-MM-DD [dateX]
if (source.Length == 10)
{
- // Just a calendar date
+ parseData.IsCalendarDateOnly = true;
return true;
}
--- /dev/null
+// 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>
+ {
+ 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)
+ {
+ 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);
+ }
+ }
+}
_namingPolicy = namingPolicy;
_nameCache = new ConcurrentDictionary<ulong, JsonEncodedText>();
-#if NET6_0_OR_GREATER
+#if NETCOREAPP
string[] names = Enum.GetNames<T>();
T[] values = Enum.GetValues<T>();
#else
break;
}
-#if NET6_0_OR_GREATER
+#if NETCOREAPP
T value = values[i];
#else
T value = (T)values.GetValue(i)!;
--- /dev/null
+// 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>
+ {
+ 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);
+
+ 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);
+ }
+ }
+}
type == typeof(IntPtr) ||
type == typeof(UIntPtr) ||
// Exlude delegates.
- typeof(Delegate).IsAssignableFrom(type) ||
- // DateOnly/TimeOnly support to be added in future releases;
- // guard against invalid object-based serializations for now.
- // cf. https://github.com/dotnet/runtime/issues/53539
- //
- // For simplicity we elide equivalent checks for targets
- // that are older than net6.0, since they do not include
- // DateOnly or TimeOnly.
-#if NETCOREAPP
- type == typeof(DateOnly) ||
- type == typeof(TimeOnly);
-#else
- false;
-#endif
+ typeof(Delegate).IsAssignableFrom(type);
}
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
ref WriteStack state)
{
if (
-#if NET5_0_OR_GREATER
+#if NETCOREAPP
// Short-circuit the check against "is not null"; treated as a constant by recent versions of the JIT.
typeof(T).IsValueType)
#else
private static Dictionary<Type, JsonConverter> GetDefaultSimpleConverters()
{
- const int NumberOfSimpleConverters = 24;
+ const int NumberOfSimpleConverters = 26;
var converters = new Dictionary<Type, JsonConverter>(NumberOfSimpleConverters);
// Use a dictionary for simple converters.
Add(JsonMetadataServices.CharConverter);
Add(JsonMetadataServices.DateTimeConverter);
Add(JsonMetadataServices.DateTimeOffsetConverter);
+#if NETCOREAPP
+ Add(JsonMetadataServices.DateOnlyConverter);
+ Add(JsonMetadataServices.TimeOnlyConverter);
+#endif
Add(JsonMetadataServices.DoubleConverter);
Add(JsonMetadataServices.DecimalConverter);
Add(JsonMetadataServices.GuidConverter);
Add(JsonMetadataServices.UriConverter);
Add(JsonMetadataServices.VersionConverter);
- Debug.Assert(NumberOfSimpleConverters == converters.Count);
+ Debug.Assert(converters.Count <= NumberOfSimpleConverters);
return converters;
public static JsonConverter<DateTimeOffset> DateTimeOffsetConverter => s_dateTimeOffsetConverter ??= new DateTimeOffsetConverter();
private static JsonConverter<DateTimeOffset>? s_dateTimeOffsetConverter;
+#if NETCOREAPP
+ /// <summary>
+ /// Returns a <see cref="JsonConverter{T}"/> instance that converts <see cref="DateOnly"/> values.
+ /// </summary>
+ /// <remarks>This API is for use by the output of the System.Text.Json source generator and should not be called directly.</remarks>
+ public static JsonConverter<DateOnly> DateOnlyConverter => s_dateOnlyConverter ??= new DateOnlyConverter();
+ private static JsonConverter<DateOnly>? s_dateOnlyConverter;
+
+ /// <summary>
+ /// Returns a <see cref="JsonConverter{T}"/> instance that converts <see cref="TimeOnly"/> values.
+ /// </summary>
+ /// <remarks>This API is for use by the output of the System.Text.Json source generator and should not be called directly.</remarks>
+ public static JsonConverter<TimeOnly> TimeOnlyConverter => s_timeOnlyConverter ??= new TimeOnlyConverter();
+ private static JsonConverter<TimeOnly>? s_timeOnlyConverter;
+#endif
+
/// <summary>
/// Returns a <see cref="JsonConverter{T}"/> instance that converts <see cref="decimal"/> values.
/// </summary>
}
[DoesNotReturn]
- public static void ThrowFormatException(DataType dateType)
+ public static void ThrowFormatException(DataType dataType)
{
string message = "";
- switch (dateType)
+ switch (dataType)
{
case DataType.Boolean:
- message = SR.FormatBoolean;
- break;
+ case DataType.DateOnly:
case DataType.DateTime:
- message = SR.FormatDateTime;
- break;
case DataType.DateTimeOffset:
- message = SR.FormatDateTimeOffset;
- break;
case DataType.TimeSpan:
- message = SR.FormatTimeSpan;
+ case DataType.Guid:
+ case DataType.Version:
+ message = SR.Format(SR.UnsupportedFormat, dataType);
break;
case DataType.Base64String:
message = SR.CannotDecodeInvalidBase64;
break;
- case DataType.Guid:
- message = SR.FormatGuid;
- break;
- case DataType.Version:
- message = SR.FormatVersion;
- break;
default:
- Debug.Fail($"The DateType enum value: {dateType} is not part of the switch. Add the appropriate case and exception message.");
+ Debug.Fail($"The DataType enum value: {dataType} is not part of the switch. Add the appropriate case and exception message.");
break;
}
internal enum DataType
{
Boolean,
+ DateOnly,
DateTime,
DateTimeOffset,
TimeSpan,
await RunTest<IntPtr>(json);
await RunTest<IntPtr?>(json); // One nullable variation.
await RunTest<UIntPtr>(json);
-#if NETCOREAPP
- await RunTest<DateOnly>(json);
- await RunTest<TimeOnly>(json);
-#endif
async Task RunTest<T>(string json)
{
await RunTest((IntPtr)123);
await RunTest<IntPtr?>(new IntPtr(123)); // One nullable variation.
await RunTest((UIntPtr)123);
-#if NETCOREAPP
- await RunTest(DateOnly.MaxValue);
- await RunTest(TimeOnly.MinValue);
-#endif
async Task RunTest<T>(T value)
{
public JsonTypeInfo<JsonElement> JsonElement { get; }
public JsonTypeInfo<RealWorldContextTests.ClassWithEnumAndNullable> ClassWithEnumAndNullable { get; }
public JsonTypeInfo<RealWorldContextTests.ClassWithNullableProperties> ClassWithNullableProperties { get; }
+#if NETCOREAPP
+ public JsonTypeInfo<RealWorldContextTests.ClassWithDateOnlyAndTimeOnlyValues> ClassWithDateOnlyAndTimeOnlyValues { get; }
+#endif
public JsonTypeInfo<ClassWithCustomConverter> ClassWithCustomConverter { get; }
public JsonTypeInfo<StructWithCustomConverter> StructWithCustomConverter { get; }
public JsonTypeInfo<ClassWithCustomConverterFactory> ClassWithCustomConverterFactory { get; }
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))]
[JsonSerializable(typeof(RealWorldContextTests.ClassWithNullableProperties))]
+#if NETCOREAPP
+ [JsonSerializable(typeof(RealWorldContextTests.ClassWithDateOnlyAndTimeOnlyValues))]
+#endif
[JsonSerializable(typeof(ClassWithCustomConverter))]
[JsonSerializable(typeof(StructWithCustomConverter))]
[JsonSerializable(typeof(ClassWithCustomConverterFactory))]
[JsonSerializable(typeof(JsonElement), GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(RealWorldContextTests.ClassWithNullableProperties), GenerationMode = JsonSourceGenerationMode.Metadata)]
+#if NETCOREAPP
+ [JsonSerializable(typeof(RealWorldContextTests.ClassWithDateOnlyAndTimeOnlyValues), GenerationMode = JsonSourceGenerationMode.Metadata)]
+#endif
[JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))]
[JsonSerializable(typeof(RealWorldContextTests.ClassWithNullableProperties))]
+#if NETCOREAPP
+ [JsonSerializable(typeof(RealWorldContextTests.ClassWithDateOnlyAndTimeOnlyValues))]
+#endif
[JsonSerializable(typeof(ClassWithCustomConverter))]
[JsonSerializable(typeof(StructWithCustomConverter))]
[JsonSerializable(typeof(ClassWithCustomConverterFactory))]
[JsonSerializable(typeof(JsonElement), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(RealWorldContextTests.ClassWithNullableProperties), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
+#if NETCOREAPP
+ [JsonSerializable(typeof(RealWorldContextTests.ClassWithDateOnlyAndTimeOnlyValues), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
+#endif
[JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
}
}
+#if NETCOREAPP
+ [Fact]
+ public virtual void ClassWithDateOnlyAndTimeOnlyValues_Roundtrip()
+ {
+ RunTest(new ClassWithDateOnlyAndTimeOnlyValues
+ {
+ DateOnly = DateOnly.Parse("2022-05-10"),
+ NullableDateOnly = DateOnly.Parse("2022-05-10"),
+
+ TimeOnly = TimeOnly.Parse("21:51:51"),
+ NullableTimeOnly = TimeOnly.Parse("21:51:51"),
+ });
+
+ RunTest(new ClassWithDateOnlyAndTimeOnlyValues());
+
+ void RunTest(ClassWithDateOnlyAndTimeOnlyValues expected)
+ {
+ string json = JsonSerializer.Serialize(expected, DefaultContext.ClassWithDateOnlyAndTimeOnlyValues);
+ ClassWithDateOnlyAndTimeOnlyValues actual = JsonSerializer.Deserialize(json, DefaultContext.ClassWithDateOnlyAndTimeOnlyValues);
+
+ Assert.Equal(expected.DateOnly, actual.DateOnly);
+ Assert.Equal(expected.NullableDateOnly, actual.NullableDateOnly);
+
+ Assert.Equal(expected.TimeOnly, actual.TimeOnly);
+ Assert.Equal(expected.NullableTimeOnly, actual.NullableTimeOnly);
+ }
+ }
+
+ public class ClassWithDateOnlyAndTimeOnlyValues
+ {
+ public DateOnly DateOnly { get; set; }
+ public DateOnly? NullableDateOnly { get; set; }
+
+ public TimeOnly TimeOnly { get; set; }
+ public TimeOnly? NullableTimeOnly { get; set; }
+ }
+#endif
+
public class ClassWithNullableProperties
{
public Uri Uri { get; set; }
[JsonSerializable(typeof(ClassThatImplementsIAsyncEnumerable))]
[JsonSerializable(typeof(ClassWithType<ClassThatImplementsIAsyncEnumerable>))]
[JsonSerializable(typeof(ClassWithAsyncEnumerableConverter))]
-
-#if NETCOREAPP
- [JsonSerializable(typeof(DateOnly))]
- [JsonSerializable(typeof(ClassWithType<DateOnly>))]
- [JsonSerializable(typeof(TimeOnly))]
- [JsonSerializable(typeof(ClassWithType<TimeOnly>))]
-#endif
internal sealed partial class UnsupportedTypesTestsContext_Metadata : JsonSerializerContext
{
}
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable))]
[JsonSerializable(typeof(RealWorldContextTests.ClassWithNullableProperties))]
+#if NETCOREAPP
+ [JsonSerializable(typeof(RealWorldContextTests.ClassWithDateOnlyAndTimeOnlyValues))]
+#endif
[JsonSerializable(typeof(ClassWithCustomConverter))]
[JsonSerializable(typeof(StructWithCustomConverter))]
[JsonSerializable(typeof(ClassWithCustomConverterFactory))]
[JsonSerializable(typeof(JsonElement), GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(RealWorldContextTests.ClassWithNullableProperties), GenerationMode = JsonSourceGenerationMode.Serialization)]
+#if NETCOREAPP
+ [JsonSerializable(typeof(RealWorldContextTests.ClassWithDateOnlyAndTimeOnlyValues), GenerationMode = JsonSourceGenerationMode.Serialization)]
+#endif
[JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(JsonElement), GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(RealWorldContextTests.ClassWithEnumAndNullable), GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(RealWorldContextTests.ClassWithNullableProperties), GenerationMode = JsonSourceGenerationMode.Serialization)]
+#if NETCOREAPP
+ [JsonSerializable(typeof(RealWorldContextTests.ClassWithDateOnlyAndTimeOnlyValues), GenerationMode = JsonSourceGenerationMode.Serialization)]
+#endif
[JsonSerializable(typeof(ClassWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(StructWithCustomConverter), GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(ClassWithCustomConverterFactory), GenerationMode = JsonSourceGenerationMode.Serialization)]
}
}
+#if NETCOREAPP
+ [Fact]
+ public override void ClassWithDateOnlyAndTimeOnlyValues_Roundtrip()
+ {
+ RunTest(new ClassWithDateOnlyAndTimeOnlyValues
+ {
+ DateOnly = DateOnly.Parse("2022-05-10"),
+ NullableDateOnly = DateOnly.Parse("2022-05-10"),
+
+ TimeOnly = TimeOnly.Parse("21:51:51"),
+ NullableTimeOnly = TimeOnly.Parse("21:51:51"),
+ });
+
+ RunTest(new ClassWithDateOnlyAndTimeOnlyValues());
+
+ void RunTest(ClassWithDateOnlyAndTimeOnlyValues expected)
+ {
+ string json = JsonSerializer.Serialize(expected, DefaultContext.ClassWithDateOnlyAndTimeOnlyValues);
+ ClassWithDateOnlyAndTimeOnlyValues actual = JsonSerializer.Deserialize(json, ((ITestContext)MetadataWithPerTypeAttributeContext.Default).ClassWithDateOnlyAndTimeOnlyValues);
+
+ Assert.Equal(expected.DateOnly, actual.DateOnly);
+ Assert.Equal(expected.NullableDateOnly, actual.NullableDateOnly);
+
+ Assert.Equal(expected.TimeOnly, actual.TimeOnly);
+ Assert.Equal(expected.NullableTimeOnly, actual.NullableTimeOnly);
+ }
+ }
+#endif
+
[Fact]
public override void ParameterizedConstructor()
{
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Generic;
using System.Globalization;
using System.Runtime.CompilerServices;
using Newtonsoft.Json;
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<TimeSpan>(json));
}
+
+#if NETCOREAPP
+ [Theory]
+ [InlineData("1970-01-01")]
+ [InlineData("2002-02-13")]
+ [InlineData("2022-05-10")]
+ [InlineData("\\u0032\\u0030\\u0032\\u0032\\u002D\\u0030\\u0035\\u002D\\u0031\\u0030", "2022-05-10")]
+ [InlineData("0001-01-01")] // DateOnly.MinValue
+ [InlineData("9999-12-31")] // DateOnly.MaxValue
+ public static void DateOnly_Read_Success(string json, string? actual = null)
+ {
+ DateOnly value = JsonSerializer.Deserialize<DateOnly>($"\"{json}\"");
+ Assert.Equal(DateOnly.Parse(actual ?? json), value);
+ }
+
+ [Theory]
+ [InlineData("1970-01-01")]
+ [InlineData("2002-02-13")]
+ [InlineData("2022-05-10")]
+ [InlineData("\\u0032\\u0030\\u0032\\u0032\\u002D\\u0030\\u0035\\u002D\\u0031\\u0030", "2022-05-10")]
+ [InlineData("0001-01-01")] // DateOnly.MinValue
+ [InlineData("9999-12-31")] // DateOnly.MaxValue
+ public static void DateOnly_ReadDictionaryKey_Success(string json, string? actual = null)
+ {
+ Dictionary<DateOnly, int> expectedDict = new() { [DateOnly.Parse(actual ?? json)] = 0 };
+ Dictionary<DateOnly, int> actualDict = JsonSerializer.Deserialize<Dictionary<DateOnly, int>>($@"{{""{json}"":0}}");
+ Assert.Equal(expectedDict, actualDict);
+ }
+
+ [Fact]
+ public static void DateOnly_Read_Nullable_Tests()
+ {
+ DateOnly? value = JsonSerializer.Deserialize<DateOnly?>("null");
+ Assert.False(value.HasValue);
+
+ value = JsonSerializer.Deserialize<DateOnly?>("\"2022-05-10\"");
+ Assert.True(value.HasValue);
+ Assert.Equal(DateOnly.Parse("2022-05-10"), value);
+ Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<DateOnly>("null"));
+ }
+
+ [Theory]
+ [InlineData("05/10/2022")] // 'd' Format
+ [InlineData("Tue, 10 May 2022")] // 'r' Format
+ [InlineData("\t2022-05-10")] // Otherwise valid but has invalid json character
+ [InlineData("\\t2022-05-10")] // Otherwise valid but has leading whitespace
+ [InlineData("2022-05-10 ")] // Otherwise valid but has trailing whitespace
+ // Fail on arbitrary ISO dates
+ [InlineData("2022-05-10T20:53:01")]
+ [InlineData("2022-05-10T20:53:01.3552286")]
+ [InlineData("2022-05-10T20:53:01.3552286+01:00")]
+ [InlineData("2022-05-10T20:53Z")]
+ [InlineData("\\u0030\\u0035\\u002F\\u0031\\u0030\\u002F\\u0032\\u0030\\u0032\\u0032")]
+ [InlineData("00:00:01")]
+ [InlineData("23:59:59")]
+ [InlineData("23:59:59.00000009")]
+ [InlineData("1.00:00:00")]
+ [InlineData("1:2:00:00")]
+ [InlineData("+00:00:00")]
+ [InlineData("1$")]
+ [InlineData("-2020-05-10")]
+ [InlineData("0000-12-31")] // DateOnly.MinValue - 1
+ [InlineData("10000-01-01")] // DateOnly.MaxValue + 1
+ [InlineData("1234", false)]
+ [InlineData("{}", false)]
+ [InlineData("[]", false)]
+ [InlineData("true", false)]
+ [InlineData("null", false)]
+ public static void DateOnly_Read_Failure(string json, bool addQuotes = true)
+ {
+ if (addQuotes)
+ json = $"\"{json}\"";
+
+ Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<DateOnly>(json));
+ }
+
+ [Theory]
+ [InlineData("23:59:59")]
+ [InlineData("23:59:59.9", "23:59:59.9000000")]
+ [InlineData("02:48:05.4775807")]
+ [InlineData("02:48:05.4775808")]
+ [InlineData("\\u0032\\u0033\\u003A\\u0035\\u0039\\u003A\\u0035\\u0039", "23:59:59")]
+ [InlineData("00:00:00.0000000", "00:00:00")] // TimeOnly.MinValue
+ [InlineData("23:59:59.9999999")] // TimeOnly.MaxValue
+ public static void TimeOnly_Read_Success(string json, string? actual = null)
+ {
+ TimeOnly value = JsonSerializer.Deserialize<TimeOnly>($"\"{json}\"");
+ Assert.Equal(TimeOnly.Parse(actual ?? json), value);
+ }
+
+ [Fact]
+ public static void TimeOnly_Read_Nullable_Tests()
+ {
+ TimeOnly? value = JsonSerializer.Deserialize<TimeOnly?>("null");
+ Assert.False(value.HasValue);
+
+ value = JsonSerializer.Deserialize<TimeOnly?>("\"23:59:59\"");
+ Assert.True(value.HasValue);
+ Assert.Equal(TimeOnly.Parse("23:59:59"), value);
+ Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<TimeOnly>("null"));
+ }
+
+ [Theory]
+ [InlineData("00:00")]
+ [InlineData("23:59")]
+ [InlineData("\t23:59:59")] // Otherwise valid but has invalid json character
+ [InlineData("\\t23:59:59")] // Otherwise valid but has leading whitespace
+ [InlineData("23:59:59 ")] // Otherwise valid but has trailing whitespace
+ [InlineData("\\u0032\\u0034\\u003A\\u0030\\u0030\\u003A\\u0030\\u0030")]
+ [InlineData("00:60:00")]
+ [InlineData("00:00:60")]
+ [InlineData("00:00:00.00000009")]
+ [InlineData("900000000.00:00:00")]
+ [InlineData("1:00:00")] // 'g' Format
+ [InlineData("1:2:00:00")] // 'g' Format
+ [InlineData("+00:00:00")]
+ [InlineData("2021-06-18")]
+ [InlineData("1$")]
+ [InlineData("-00:00:00.0000001")] // TimeOnly.MinValue - 1
+ [InlineData("24:00:00.0000000")] // TimeOnly.MaxValue + 1
+ [InlineData("10675199.02:48:05.4775807")] // TimeSpan.MaxValue
+ [InlineData("-10675199.02:48:05.4775808")] // TimeSpan.MinValue
+ [InlineData("1234", false)]
+ [InlineData("{}", false)]
+ [InlineData("[]", false)]
+ [InlineData("true", false)]
+ [InlineData("null", false)]
+ public static void TimeOnly_Read_Failure(string json, bool addQuotes = true)
+ {
+ if (addQuotes)
+ json = $"\"{json}\"";
+
+ Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<TimeOnly>(json));
+ }
+#endif
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Generic;
using System.Text.Encodings.Web;
using Newtonsoft.Json;
using Xunit;
Assert.Equal($"\"{expectedValue ?? value}\"", json);
Assert.Equal(json, JsonConvert.SerializeObject(ts));
}
+
+#if NETCOREAPP
+ [Theory]
+ [InlineData("1970-01-01")]
+ [InlineData("2002-02-13")]
+ [InlineData("2022-05-10")]
+ [InlineData("0001-01-01")] // DateOnly.MinValue
+ [InlineData("9999-12-31")] // DateOnly.MaxValue
+ public static void DateOnly_Write_Success(string value)
+ {
+ DateOnly ts = DateOnly.Parse(value);
+ string json = JsonSerializer.Serialize(ts);
+ Assert.Equal($"\"{value}\"", json);
+ }
+
+ [Theory]
+ [InlineData("1970-01-01")]
+ [InlineData("2002-02-13")]
+ [InlineData("2022-05-10")]
+ [InlineData("0001-01-01")] // DateOnly.MinValue
+ [InlineData("9999-12-31")] // DateOnly.MaxValue
+ public static void DateOnly_WriteDictionaryKey_Success(string value)
+ {
+ var dict = new Dictionary<DateOnly, int> { [DateOnly.Parse(value)] = 0 };
+ string json = JsonSerializer.Serialize(dict);
+ Assert.Equal($@"{{""{value}"":0}}", json);
+ }
+
+ [Theory]
+ [InlineData("1:59:59", "01:59:59")]
+ [InlineData("23:59:59")]
+ [InlineData("23:59:59.9", "23:59:59.9000000")]
+ [InlineData("00:00:00")] // TimeOnly.MinValue
+ [InlineData("23:59:59.9999999")] // TimeOnly.MaxValue
+ public static void TimeOnly_Write_Success(string value, string? expectedValue = null)
+ {
+ TimeOnly ts = TimeOnly.Parse(value);
+ string json = JsonSerializer.Serialize(ts);
+ Assert.Equal($"\"{expectedValue ?? value}\"", json);
+ }
+#endif
}
}