Skip to content

Commit 78cec41

Browse files
Merge pull request #433 from notion-dotnet/fix/date-serialization
Fix: date custom converter to serialize and deserialize ISO 8601 date and time
2 parents 28ccc2c + 4e9c6be commit 78cec41

File tree

4 files changed

+230
-6
lines changed

4 files changed

+230
-6
lines changed

Src/Notion.Client/Models/PropertyValue/DateCustomConverter.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
using System;
2+
using System.Globalization;
23
using Newtonsoft.Json;
34

45
namespace Notion.Client
56
{
67
public class DateCustomConverter : JsonConverter<Date>
78
{
9+
private const string DateFormat = "yyyy-MM-dd";
10+
private const string DateTimeFormat = "yyyy-MM-ddTHH:mm:ssZ";
11+
812
public override Date ReadJson(JsonReader reader, Type objectType, Date existingValue, bool hasExistingValue,
913
JsonSerializer serializer)
1014
{
@@ -39,16 +43,16 @@ public override void WriteJson(JsonWriter writer, Date value, JsonSerializer ser
3943

4044
if (value.Start.HasValue)
4145
{
42-
string startFormat = value.IncludeTime ? "yyyy-MM-ddTHH:mm:ss" : "yyyy-MM-dd";
46+
string startFormat = value.IncludeTime ? DateTimeFormat : DateFormat;
4347
writer.WritePropertyName("start");
44-
writer.WriteValue(value.Start.Value.ToString(startFormat));
48+
writer.WriteValue(value.Start.Value.ToString(startFormat, CultureInfo.InvariantCulture));
4549
}
4650

4751
if (value.End.HasValue)
4852
{
49-
string endFormat = value.IncludeTime ? "yyyy-MM-ddTHH:mm:ss" : "yyyy-MM-dd";
53+
string endFormat = value.IncludeTime ? DateTimeFormat : DateFormat;
5054
writer.WritePropertyName("end");
51-
writer.WriteValue(value.End.Value.ToString(endFormat));
55+
writer.WriteValue(value.End.Value.ToString(endFormat, CultureInfo.InvariantCulture));
5256
}
5357

5458
if (!string.IsNullOrEmpty(value.TimeZone))
@@ -71,7 +75,7 @@ public override void WriteJson(JsonWriter writer, Date value, JsonSerializer ser
7175

7276
includeTime = dateTimeString.Contains("T") || dateTimeString.Contains(" ");
7377

74-
return DateTimeOffset.Parse(dateTimeString).UtcDateTime;
78+
return DateTimeOffset.Parse(dateTimeString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).UtcDateTime;
7579
}
7680
}
7781
}

Src/Notion.Client/Models/PropertyValue/DatePropertyValue.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public class Date
4848
/// <summary>
4949
/// Whether to include time
5050
/// </summary>
51+
[JsonIgnore]
5152
public bool IncludeTime { get; set; } = true;
5253
}
5354
}

Test/Notion.UnitTests/DatabasesClientTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ var jsonData
500500
});
501501

502502
//var formulaPropertyValue = (FormulaPropertyValue)page.Properties["FormulaProp"];
503-
formulaPropertyValue.Formula.Date.Start.Should().Be(DateTime.Parse("2021-06-28"));
503+
formulaPropertyValue.Formula.Date.Start.Should().Be(DateTimeOffset.Parse("2021-06-28", null, System.Globalization.DateTimeStyles.AssumeUniversal).UtcDateTime);
504504
formulaPropertyValue.Formula.Date.End.Should().BeNull();
505505
}
506506
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
using System;
2+
using System.IO;
3+
using Newtonsoft.Json;
4+
using Notion.Client;
5+
using Xunit;
6+
7+
namespace NotionUnitTests.PropertyValue;
8+
9+
public class DateCustomConverterTests
10+
{
11+
private readonly DateCustomConverter _converter = new();
12+
private readonly JsonSerializer _serializer = new();
13+
14+
[Fact]
15+
public void Serialize_null_writes_null()
16+
{
17+
// Arrange
18+
Date date = null;
19+
var stringWriter = new StringWriter();
20+
var jsonWriter = new JsonTextWriter(stringWriter);
21+
22+
// Act
23+
_converter.WriteJson(jsonWriter, date, _serializer);
24+
jsonWriter.Flush();
25+
26+
// Assert
27+
Assert.Equal("null", stringWriter.ToString());
28+
}
29+
30+
[Fact]
31+
public void Serialize_start_date_only_produces_correct_json()
32+
{
33+
// Arrange
34+
var date = new Date
35+
{
36+
Start = new DateTimeOffset(2023, 5, 15, 0, 0, 0, TimeSpan.Zero),
37+
IncludeTime = false
38+
};
39+
40+
// Act
41+
var json = JsonConvert.SerializeObject(date);
42+
43+
// Assert
44+
Assert.Contains("\"start\":\"2023-05-15\"", json);
45+
Assert.DoesNotContain("\"end\":", json);
46+
Assert.DoesNotContain("\"time_zone\":", json);
47+
}
48+
49+
[Fact]
50+
public void Serialize_start_and_end_dates_produces_correct_json()
51+
{
52+
// Arrange
53+
var date = new Date
54+
{
55+
Start = new DateTimeOffset(2023, 5, 15, 0, 0, 0, TimeSpan.Zero),
56+
End = new DateTimeOffset(2023, 5, 20, 0, 0, 0, TimeSpan.Zero),
57+
IncludeTime = false
58+
};
59+
60+
// Act
61+
var json = JsonConvert.SerializeObject(date);
62+
63+
// Assert
64+
Assert.Contains("\"start\":\"2023-05-15\"", json);
65+
Assert.Contains("\"end\":\"2023-05-20\"", json);
66+
Assert.DoesNotContain("\"time_zone\":", json);
67+
}
68+
69+
[Fact]
70+
public void Serialize_with_time_included_formats_time_correctly()
71+
{
72+
// Arrange
73+
var date = new Date
74+
{
75+
Start = new DateTimeOffset(2023, 5, 15, 14, 30, 45, TimeSpan.Zero),
76+
IncludeTime = true
77+
};
78+
79+
// Act
80+
var json = JsonConvert.SerializeObject(date);
81+
82+
// Assert
83+
Assert.Contains("\"start\":\"2023-05-15T14:30:45Z\"", json);
84+
}
85+
86+
[Fact]
87+
public void Serialize_with_time_zone_includes_time_zone()
88+
{
89+
// Arrange
90+
var date = new Date
91+
{
92+
Start = new DateTimeOffset(2023, 5, 15, 14, 30, 45, TimeSpan.Zero),
93+
TimeZone = "Europe/London",
94+
IncludeTime = true
95+
};
96+
97+
// Act
98+
var json = JsonConvert.SerializeObject(date);
99+
100+
// Assert
101+
Assert.Contains("\"start\":\"2023-05-15T14:30:45Z\"", json);
102+
Assert.Contains("\"time_zone\":\"Europe/London\"", json);
103+
}
104+
105+
[Fact]
106+
public void Deserialize_null_returns_null()
107+
{
108+
// Arrange
109+
const string Json = "null";
110+
111+
// Act
112+
var result = JsonConvert.DeserializeObject<Date>(Json);
113+
114+
// Assert
115+
Assert.Null(result);
116+
}
117+
118+
[Fact]
119+
public void Deserialize_start_date_only_returns_correct_date()
120+
{
121+
// Arrange
122+
const string Json = "{\"start\":\"2023-05-15\"}";
123+
124+
// Act
125+
var result = JsonConvert.DeserializeObject<Date>(Json);
126+
127+
// Assert
128+
Assert.NotNull(result);
129+
Assert.Equal(new DateTimeOffset(2023, 5, 15, 0, 0, 0, TimeSpan.Zero), result.Start);
130+
Assert.Null(result.End);
131+
Assert.Null(result.TimeZone);
132+
Assert.False(result.IncludeTime);
133+
}
134+
135+
[Fact]
136+
public void Deserialize_with_time_sets_include_time_flag()
137+
{
138+
// Arrange
139+
const string Json = "{\"start\":\"2023-05-15T14:30:45\"}";
140+
141+
// Act
142+
var result = JsonConvert.DeserializeObject<Date>(Json);
143+
144+
// Assert
145+
Assert.NotNull(result);
146+
Assert.Equal(new DateTimeOffset(2023, 5, 15, 14, 30, 45, TimeSpan.Zero), result.Start);
147+
Assert.True(result.IncludeTime);
148+
}
149+
150+
[Fact]
151+
public void Deserialize_with_start_end_and_time_zone_returns_complete_date()
152+
{
153+
// Arrange
154+
const string Json = "{\"start\":\"2023-05-15T14:30:45\",\"end\":\"2023-05-20T16:45:00\",\"time_zone\":\"America/New_York\"}";
155+
156+
// Act
157+
var result = JsonConvert.DeserializeObject<Date>(Json);
158+
159+
// Assert
160+
Assert.NotNull(result);
161+
Assert.Equal(new DateTimeOffset(2023, 5, 15, 14, 30, 45, TimeSpan.Zero), result.Start);
162+
Assert.Equal(new DateTimeOffset(2023, 5, 20, 16, 45, 0, TimeSpan.Zero), result.End);
163+
Assert.Equal("America/New_York", result.TimeZone);
164+
Assert.True(result.IncludeTime);
165+
}
166+
167+
[Fact]
168+
public void Date_property_value_serialize_deserialize_maintains_data()
169+
{
170+
// Arrange
171+
var datePropertyValue = new DatePropertyValue
172+
{
173+
Date = new Date
174+
{
175+
Start = new DateTimeOffset(2023, 5, 15, 14, 30, 45, TimeSpan.Zero),
176+
End = new DateTimeOffset(2023, 5, 20, 16, 45, 0, TimeSpan.Zero),
177+
TimeZone = "Europe/Berlin",
178+
IncludeTime = true
179+
}
180+
};
181+
182+
// Act
183+
var json = JsonConvert.SerializeObject(datePropertyValue);
184+
var result = JsonConvert.DeserializeObject<DatePropertyValue>(json);
185+
186+
// Assert
187+
Assert.NotNull(result);
188+
Assert.Equal(PropertyValueType.Date, result.Type);
189+
Assert.NotNull(result.Date);
190+
Assert.Equal(datePropertyValue.Date.Start, result.Date.Start);
191+
Assert.Equal(datePropertyValue.Date.End, result.Date.End);
192+
Assert.Equal(datePropertyValue.Date.TimeZone, result.Date.TimeZone);
193+
Assert.Equal(datePropertyValue.Date.IncludeTime, result.Date.IncludeTime);
194+
}
195+
196+
[Fact]
197+
public void Round_trip_preserves_data()
198+
{
199+
// Arrange
200+
var originalDate = new Date
201+
{
202+
Start = new DateTimeOffset(2023, 5, 15, 14, 30, 45, TimeSpan.Zero),
203+
End = new DateTimeOffset(2023, 5, 20, 16, 45, 0, TimeSpan.Zero),
204+
TimeZone = "Europe/Berlin",
205+
IncludeTime = true
206+
};
207+
208+
// Act
209+
var json = JsonConvert.SerializeObject(originalDate);
210+
var deserializedDate = JsonConvert.DeserializeObject<Date>(json);
211+
212+
// Assert
213+
Assert.NotNull(deserializedDate);
214+
Assert.Equal(originalDate.Start, deserializedDate.Start);
215+
Assert.Equal(originalDate.End, deserializedDate.End);
216+
Assert.Equal(originalDate.TimeZone, deserializedDate.TimeZone);
217+
Assert.True(deserializedDate.IncludeTime);
218+
}
219+
}

0 commit comments

Comments
 (0)