Skip to content

Commit 75207c7

Browse files
CopilotScarletKuro
andcommitted
Add DateOnly and TimeOnly support with converters and tests
Co-authored-by: ScarletKuro <19953225+ScarletKuro@users.noreply.github.com>
1 parent 7d65a2f commit 75207c7

11 files changed

Lines changed: 534 additions & 22 deletions

src/Scarlet.System.Text.Json.DateTimeConverter.Tests/JsonDateTimeConverterAttributeTests.cs

Lines changed: 160 additions & 12 deletions
Large diffs are not rendered by default.

src/Scarlet.System.Text.Json.DateTimeConverter.Tests/JsonDateTimeFormatConverterTests.cs

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,22 @@ public void ReflectionBased_CompleteModel_WithFormatConverter()
1818
DateTimeProperty = new DateTime(2023, 10, 1, 12, 0, 0, DateTimeKind.Utc),
1919
NullableDateTimeProperty = new DateTime(2023, 10, 1, 12, 0, 0, DateTimeKind.Utc),
2020
DateTimeOffsetProperty = new DateTimeOffset(2023, 10, 1, 12, 0, 0, TimeSpan.Zero),
21-
NullableDateTimeOffsetProperty = new DateTimeOffset(2023, 10, 1, 12, 0, 0, TimeSpan.Zero)
21+
NullableDateTimeOffsetProperty = new DateTimeOffset(2023, 10, 1, 12, 0, 0, TimeSpan.Zero),
22+
DateOnlyProperty = new DateOnly(2023, 10, 1),
23+
NullableDateOnlyProperty = new DateOnly(2023, 10, 1),
24+
TimeOnlyProperty = new TimeOnly(14, 30, 45),
25+
NullableTimeOnlyProperty = new TimeOnly(14, 30, 45)
2226
};
2327
const string expectedJson = """
2428
{
2529
"DateTimeProperty": "2023-10-01T12:00:00",
2630
"NullableDateTimeProperty": "2023-10-01T12:00:00",
2731
"DateTimeOffsetProperty": "2023-10-01T12:00:00.000Z",
28-
"NullableDateTimeOffsetProperty": "2023-10-01T12:00:00.000Z"
32+
"NullableDateTimeOffsetProperty": "2023-10-01T12:00:00.000Z",
33+
"DateOnlyProperty": "10/01/2023",
34+
"NullableDateOnlyProperty": "10/01/2023",
35+
"TimeOnlyProperty": "14.30.45",
36+
"NullableTimeOnlyProperty": "14.30.45"
2937
}
3038
""";
3139

@@ -39,6 +47,10 @@ public void ReflectionBased_CompleteModel_WithFormatConverter()
3947
Assert.Equal(originalModel.NullableDateTimeProperty, deserializedModel.NullableDateTimeProperty);
4048
Assert.Equal(originalModel.DateTimeOffsetProperty, deserializedModel.DateTimeOffsetProperty);
4149
Assert.Equal(originalModel.NullableDateTimeOffsetProperty, deserializedModel.NullableDateTimeOffsetProperty);
50+
Assert.Equal(originalModel.DateOnlyProperty, deserializedModel.DateOnlyProperty);
51+
Assert.Equal(originalModel.NullableDateOnlyProperty, deserializedModel.NullableDateOnlyProperty);
52+
Assert.Equal(originalModel.TimeOnlyProperty, deserializedModel.TimeOnlyProperty);
53+
Assert.Equal(originalModel.NullableTimeOnlyProperty, deserializedModel.NullableTimeOnlyProperty);
4254
Assert.Equal(expectedJson, json);
4355
}
4456

@@ -55,14 +67,22 @@ public void ReflectionBased_CompleteModel_WithFormatConverter_WithNullValues()
5567
DateTimeProperty = new DateTime(2023, 10, 1, 12, 0, 0, DateTimeKind.Utc),
5668
NullableDateTimeProperty = null,
5769
DateTimeOffsetProperty = new DateTimeOffset(2023, 10, 1, 12, 0, 0, TimeSpan.Zero),
58-
NullableDateTimeOffsetProperty = null
70+
NullableDateTimeOffsetProperty = null,
71+
DateOnlyProperty = new DateOnly(2023, 10, 1),
72+
NullableDateOnlyProperty = null,
73+
TimeOnlyProperty = new TimeOnly(14, 30, 45),
74+
NullableTimeOnlyProperty = null
5975
};
6076
const string expectedJson = """
6177
{
6278
"DateTimeProperty": "2023-10-01T12:00:00",
6379
"NullableDateTimeProperty": null,
6480
"DateTimeOffsetProperty": "2023-10-01T12:00:00.000Z",
65-
"NullableDateTimeOffsetProperty": null
81+
"NullableDateTimeOffsetProperty": null,
82+
"DateOnlyProperty": "10/01/2023",
83+
"NullableDateOnlyProperty": null,
84+
"TimeOnlyProperty": "14.30.45",
85+
"NullableTimeOnlyProperty": null
6686
}
6787
""";
6888

@@ -76,6 +96,10 @@ public void ReflectionBased_CompleteModel_WithFormatConverter_WithNullValues()
7696
Assert.Null(deserializedModel.NullableDateTimeProperty);
7797
Assert.Equal(originalModel.DateTimeOffsetProperty, deserializedModel.DateTimeOffsetProperty);
7898
Assert.Null(deserializedModel.NullableDateTimeOffsetProperty);
99+
Assert.Equal(originalModel.DateOnlyProperty, deserializedModel.DateOnlyProperty);
100+
Assert.Null(deserializedModel.NullableDateOnlyProperty);
101+
Assert.Equal(originalModel.TimeOnlyProperty, deserializedModel.TimeOnlyProperty);
102+
Assert.Null(deserializedModel.NullableTimeOnlyProperty);
79103
Assert.Equal(expectedJson, json);
80104
}
81105

@@ -90,14 +114,22 @@ public void SourceGenerator_CompleteModel_WithFormatConverter()
90114
DateTimeProperty = new DateTime(2023, 10, 1, 12, 0, 0, DateTimeKind.Utc),
91115
NullableDateTimeProperty = new DateTime(2023, 10, 1, 12, 0, 0, DateTimeKind.Utc),
92116
DateTimeOffsetProperty = new DateTimeOffset(2023, 10, 1, 12, 0, 0, TimeSpan.Zero),
93-
NullableDateTimeOffsetProperty = new DateTimeOffset(2023, 10, 1, 12, 0, 0, TimeSpan.Zero)
117+
NullableDateTimeOffsetProperty = new DateTimeOffset(2023, 10, 1, 12, 0, 0, TimeSpan.Zero),
118+
DateOnlyProperty = new DateOnly(2023, 10, 1),
119+
NullableDateOnlyProperty = new DateOnly(2023, 10, 1),
120+
TimeOnlyProperty = new TimeOnly(14, 30, 45),
121+
NullableTimeOnlyProperty = new TimeOnly(14, 30, 45)
94122
};
95123
const string expectedJson = """
96124
{
97125
"DateTimeProperty": "2023-10-01T12:00:00",
98126
"NullableDateTimeProperty": "2023-10-01T12:00:00",
99127
"DateTimeOffsetProperty": "2023-10-01T12:00:00.000Z",
100-
"NullableDateTimeOffsetProperty": "2023-10-01T12:00:00.000Z"
128+
"NullableDateTimeOffsetProperty": "2023-10-01T12:00:00.000Z",
129+
"DateOnlyProperty": "10/01/2023",
130+
"NullableDateOnlyProperty": "10/01/2023",
131+
"TimeOnlyProperty": "14.30.45",
132+
"NullableTimeOnlyProperty": "14.30.45"
101133
}
102134
""";
103135

@@ -111,6 +143,10 @@ public void SourceGenerator_CompleteModel_WithFormatConverter()
111143
Assert.Equal(originalModel.NullableDateTimeProperty, deserializedModel.NullableDateTimeProperty);
112144
Assert.Equal(originalModel.DateTimeOffsetProperty, deserializedModel.DateTimeOffsetProperty);
113145
Assert.Equal(originalModel.NullableDateTimeOffsetProperty, deserializedModel.NullableDateTimeOffsetProperty);
146+
Assert.Equal(originalModel.DateOnlyProperty, deserializedModel.DateOnlyProperty);
147+
Assert.Equal(originalModel.NullableDateOnlyProperty, deserializedModel.NullableDateOnlyProperty);
148+
Assert.Equal(originalModel.TimeOnlyProperty, deserializedModel.TimeOnlyProperty);
149+
Assert.Equal(originalModel.NullableTimeOnlyProperty, deserializedModel.NullableTimeOnlyProperty);
114150
Assert.Equal(expectedJson, json);
115151
}
116152

@@ -125,14 +161,22 @@ public void SourceGenerator_CompleteModel_WithFormatConverter_WithNullValues()
125161
DateTimeProperty = new DateTime(2023, 10, 1, 12, 0, 0, DateTimeKind.Utc),
126162
NullableDateTimeProperty = null,
127163
DateTimeOffsetProperty = new DateTimeOffset(2023, 10, 1, 12, 0, 0, TimeSpan.Zero),
128-
NullableDateTimeOffsetProperty = null
164+
NullableDateTimeOffsetProperty = null,
165+
DateOnlyProperty = new DateOnly(2023, 10, 1),
166+
NullableDateOnlyProperty = null,
167+
TimeOnlyProperty = new TimeOnly(14, 30, 45),
168+
NullableTimeOnlyProperty = null
129169
};
130170
const string expectedJson = """
131171
{
132172
"DateTimeProperty": "2023-10-01T12:00:00",
133173
"NullableDateTimeProperty": null,
134174
"DateTimeOffsetProperty": "2023-10-01T12:00:00.000Z",
135-
"NullableDateTimeOffsetProperty": null
175+
"NullableDateTimeOffsetProperty": null,
176+
"DateOnlyProperty": "10/01/2023",
177+
"NullableDateOnlyProperty": null,
178+
"TimeOnlyProperty": "14.30.45",
179+
"NullableTimeOnlyProperty": null
136180
}
137181
""";
138182

@@ -146,6 +190,10 @@ public void SourceGenerator_CompleteModel_WithFormatConverter_WithNullValues()
146190
Assert.Null(deserializedModel.NullableDateTimeProperty);
147191
Assert.Equal(originalModel.DateTimeOffsetProperty, deserializedModel.DateTimeOffsetProperty);
148192
Assert.Null(deserializedModel.NullableDateTimeOffsetProperty);
193+
Assert.Equal(originalModel.DateOnlyProperty, deserializedModel.DateOnlyProperty);
194+
Assert.Null(deserializedModel.NullableDateOnlyProperty);
195+
Assert.Equal(originalModel.TimeOnlyProperty, deserializedModel.TimeOnlyProperty);
196+
Assert.Null(deserializedModel.NullableTimeOnlyProperty);
149197
Assert.Equal(expectedJson, json);
150198
}
151199
}

src/Scarlet.System.Text.Json.DateTimeConverter.Tests/Model/ReflectionBasedModel.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,16 @@ public class ReflectionBasedModel
1818

1919
[JsonDateTimeConverter("yyyy-MM-ddTHH:mm:ss.fffZ")]
2020
public DateTimeOffset? NullableDateTimeOffsetProperty { get; set; }
21+
22+
[JsonDateTimeConverter("MM/dd/yyyy")]
23+
public DateOnly DateOnlyProperty { get; set; }
24+
25+
[JsonDateTimeConverter("MM/dd/yyyy")]
26+
public DateOnly? NullableDateOnlyProperty { get; set; }
27+
28+
[JsonDateTimeConverter("HH.mm.ss")]
29+
public TimeOnly TimeOnlyProperty { get; set; }
30+
31+
[JsonDateTimeConverter("HH.mm.ss")]
32+
public TimeOnly? NullableTimeOnlyProperty { get; set; }
2133
}

src/Scarlet.System.Text.Json.DateTimeConverter.Tests/Model/SourceGeneratorWithConverterModel.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ public class SourceGeneratorWithConverterModel
2020

2121
[JsonConverter(typeof(JsonDateTimeFormatConverter<JsonDateTimeFormat.DateTimeOffsetFormat>))]
2222
public DateTimeOffset? NullableDateTimeOffsetProperty { get; set; }
23+
24+
[JsonConverter(typeof(JsonDateTimeFormatConverter<JsonDateTimeFormat.DateOnlyFormat>))]
25+
public DateOnly DateOnlyProperty { get; set; }
26+
27+
[JsonConverter(typeof(JsonDateTimeFormatConverter<JsonDateTimeFormat.DateOnlyFormat>))]
28+
public DateOnly? NullableDateOnlyProperty { get; set; }
29+
30+
[JsonConverter(typeof(JsonDateTimeFormatConverter<JsonDateTimeFormat.TimeOnlyFormat>))]
31+
public TimeOnly TimeOnlyProperty { get; set; }
32+
33+
[JsonConverter(typeof(JsonDateTimeFormatConverter<JsonDateTimeFormat.TimeOnlyFormat>))]
34+
public TimeOnly? NullableTimeOnlyProperty { get; set; }
2335
}
2436

2537
internal class JsonDateTimeFormat
@@ -32,4 +44,12 @@ internal class DateTimeFormat : IJsonDateTimeFormat
3244
{
3345
public static string Format => "yyyy-MM-ddTHH:mm:ss";
3446
}
47+
internal class DateOnlyFormat : IJsonDateTimeFormat
48+
{
49+
public static string Format => "MM/dd/yyyy";
50+
}
51+
internal class TimeOnlyFormat : IJsonDateTimeFormat
52+
{
53+
public static string Format => "HH.mm.ss";
54+
}
3555
}

src/Scarlet.System.Text.Json.DateTimeConverter.Tests/Model/SourceGeneratorWithResolverFormatModel.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,16 @@ public class SourceGeneratorWithResolverFormatModel
1818

1919
[JsonDateTimeFormat("yyyy-MM-ddTHH:mm:ss.fffZ")]
2020
public DateTimeOffset? NullableDateTimeOffsetProperty { get; set; }
21+
22+
[JsonDateTimeFormat("MM/dd/yyyy")]
23+
public DateOnly DateOnlyProperty { get; set; }
24+
25+
[JsonDateTimeFormat("MM/dd/yyyy")]
26+
public DateOnly? NullableDateOnlyProperty { get; set; }
27+
28+
[JsonDateTimeFormat("HH.mm.ss")]
29+
public TimeOnly TimeOnlyProperty { get; set; }
30+
31+
[JsonDateTimeFormat("HH.mm.ss")]
32+
public TimeOnly? NullableTimeOnlyProperty { get; set; }
2133
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System.Globalization;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
5+
namespace Scarlet.System.Text.Json.DateTimeConverter.Converters;
6+
7+
/// <summary>
8+
/// Converts <see cref="DateOnly"/> objects to and from JSON using a specified date format.
9+
/// </summary>
10+
internal sealed class DateOnlyConverter : JsonConverter<DateOnly>
11+
{
12+
private readonly string _format;
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="DateOnlyConverter"/> class with the specified date format.
16+
/// </summary>
17+
/// <param name="format">The date format string.</param>
18+
private DateOnlyConverter(string format) => _format = format;
19+
20+
/// <summary>
21+
/// Reads and converts the JSON to a <see cref="DateOnly"/> object.
22+
/// </summary>
23+
/// <param name="reader">The <see cref="Utf8JsonReader"/> to read from.</param>
24+
/// <param name="typeToConvert">The type to convert.</param>
25+
/// <param name="options">Options to control the conversion behavior.</param>
26+
/// <returns>The converted <see cref="DateOnly"/> object.</returns>
27+
public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
28+
{
29+
if (reader.TokenType == JsonTokenType.String)
30+
{
31+
if (DateOnly.TryParseExact(reader.GetString(), _format, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateOnly date))
32+
{
33+
return date;
34+
}
35+
}
36+
37+
// Fallback to reading as DateTime and converting to DateOnly
38+
var dateTime = reader.GetDateTime();
39+
return DateOnly.FromDateTime(dateTime);
40+
}
41+
42+
/// <summary>
43+
/// Writes a <see cref="DateOnly"/> object as a JSON string using the specified date format.
44+
/// </summary>
45+
/// <param name="writer">The <see cref="Utf8JsonWriter"/> to write to.</param>
46+
/// <param name="value">The <see cref="DateOnly"/> value to write.</param>
47+
/// <param name="options">Options to control the conversion behavior.</param>
48+
public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
49+
{
50+
ArgumentNullException.ThrowIfNull(writer);
51+
52+
var date = value.ToString(_format, CultureInfo.InvariantCulture);
53+
writer.WriteStringValue(date);
54+
}
55+
56+
public static DateOnlyConverter FromFormat(string format) => new(format);
57+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System.Globalization;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
5+
namespace Scarlet.System.Text.Json.DateTimeConverter.Converters;
6+
7+
/// <summary>
8+
/// Converts nullable <see cref="DateOnly"/> objects to and from JSON using a specified date format.
9+
/// </summary>
10+
internal sealed class DateOnlyNullableConverter : JsonConverter<DateOnly?>
11+
{
12+
private readonly string _format;
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="DateOnlyNullableConverter"/> class with the specified date format.
16+
/// </summary>
17+
/// <param name="format">The date format string.</param>
18+
private DateOnlyNullableConverter(string format) => _format = format;
19+
20+
/// <summary>
21+
/// Reads and converts the JSON to a nullable <see cref="DateOnly"/> object.
22+
/// </summary>
23+
/// <param name="reader">The <see cref="Utf8JsonReader"/> to read from.</param>
24+
/// <param name="typeToConvert">The type to convert.</param>
25+
/// <param name="options">Options to control the conversion behavior.</param>
26+
/// <returns>The converted nullable <see cref="DateOnly"/> object, or null if the JSON token is null.</returns>
27+
public override DateOnly? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
28+
{
29+
if (reader.TokenType == JsonTokenType.Null)
30+
{
31+
return null;
32+
}
33+
34+
if (reader.TokenType == JsonTokenType.String)
35+
{
36+
if (DateOnly.TryParseExact(reader.GetString(), _format, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateOnly date))
37+
{
38+
return date;
39+
}
40+
}
41+
42+
return null;
43+
}
44+
45+
/// <summary>
46+
/// Writes a nullable <see cref="DateOnly"/> object as a JSON string using the specified date format.
47+
/// </summary>
48+
/// <param name="writer">The <see cref="Utf8JsonWriter"/> to write to.</param>
49+
/// <param name="value">The nullable <see cref="DateOnly"/> value to write.</param>
50+
/// <param name="options">Options to control the conversion behavior.</param>
51+
public override void Write(Utf8JsonWriter writer, DateOnly? value, JsonSerializerOptions options)
52+
{
53+
ArgumentNullException.ThrowIfNull(writer);
54+
55+
if (value.HasValue)
56+
{
57+
var date = value.Value.ToString(_format, CultureInfo.InvariantCulture);
58+
writer.WriteStringValue(date);
59+
}
60+
else
61+
{
62+
writer.WriteNullValue();
63+
}
64+
}
65+
66+
public static DateOnlyNullableConverter FromFormat(string format) => new(format);
67+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System.Globalization;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
5+
namespace Scarlet.System.Text.Json.DateTimeConverter.Converters;
6+
7+
/// <summary>
8+
/// Converts <see cref="TimeOnly"/> objects to and from JSON using a specified time format.
9+
/// </summary>
10+
internal sealed class TimeOnlyConverter : JsonConverter<TimeOnly>
11+
{
12+
private readonly string _format;
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="TimeOnlyConverter"/> class with the specified time format.
16+
/// </summary>
17+
/// <param name="format">The time format string.</param>
18+
private TimeOnlyConverter(string format) => _format = format;
19+
20+
/// <summary>
21+
/// Reads and converts the JSON to a <see cref="TimeOnly"/> object.
22+
/// </summary>
23+
/// <param name="reader">The <see cref="Utf8JsonReader"/> to read from.</param>
24+
/// <param name="typeToConvert">The type to convert.</param>
25+
/// <param name="options">Options to control the conversion behavior.</param>
26+
/// <returns>The converted <see cref="TimeOnly"/> object.</returns>
27+
public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
28+
{
29+
if (reader.TokenType == JsonTokenType.String)
30+
{
31+
if (TimeOnly.TryParseExact(reader.GetString(), _format, CultureInfo.InvariantCulture, DateTimeStyles.None, out TimeOnly time))
32+
{
33+
return time;
34+
}
35+
}
36+
37+
// Fallback to reading as DateTime and converting to TimeOnly
38+
var dateTime = reader.GetDateTime();
39+
return TimeOnly.FromDateTime(dateTime);
40+
}
41+
42+
/// <summary>
43+
/// Writes a <see cref="TimeOnly"/> object as a JSON string using the specified time format.
44+
/// </summary>
45+
/// <param name="writer">The <see cref="Utf8JsonWriter"/> to write to.</param>
46+
/// <param name="value">The <see cref="TimeOnly"/> value to write.</param>
47+
/// <param name="options">Options to control the conversion behavior.</param>
48+
public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options)
49+
{
50+
ArgumentNullException.ThrowIfNull(writer);
51+
52+
var time = value.ToString(_format, CultureInfo.InvariantCulture);
53+
writer.WriteStringValue(time);
54+
}
55+
56+
public static TimeOnlyConverter FromFormat(string format) => new(format);
57+
}

0 commit comments

Comments
 (0)