Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/Twilio/Converters/CustomConverters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,21 @@
}
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)

Check failure on line 30 in src/Twilio/Converters/CustomConverters.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 19 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=twilio_twilio-csharp&issues=AZ3OGqhvDEWyCvtato42&open=AZ3OGqhvDEWyCvtato42&pullRequest=827
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}
if (reader.TokenType == JsonToken.Date)
{
var dt = ConvertToDateTime(reader.Value);
if (objectType == typeof(List<DateTime>))
{
return new List<DateTime> { dt };
}
return dt;
}
Comment on lines +32 to +44
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReadJson returns a DateTime for JsonToken.Date regardless of objectType. If this converter is used for List<DateTime>, a non-array JSON value (e.g., a single date) will cause the converter to return the wrong runtime type and Newtonsoft will throw during assignment. Consider branching on objectType and either (a) wrapping a single date into a singleton list for List<DateTime> or (b) throwing a clear exception when the JSON shape doesn't match the target type (and similarly avoid returning a list when objectType is DateTime?).

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +44
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Casting reader.Value directly to DateTime can throw if the serializer is configured with DateParseHandling.DateTimeOffset (in which case JsonToken.Date values are typically DateTimeOffset). Consider handling both DateTime and DateTimeOffset (and converting consistently) to avoid an InvalidCastException.

Copilot uses AI. Check for mistakes.
if (reader.TokenType == JsonToken.StartArray)
{
var dateTimes = new List<DateTime>();
Expand All @@ -42,6 +55,10 @@
dateTimes.Add(dateTime);
}
}
else if (reader.TokenType == JsonToken.Date)
{
dateTimes.Add(ConvertToDateTime(reader.Value));
}
Comment on lines 32 to +61
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior adds support for JsonToken.Null and JsonToken.Date (including date values inside arrays), but there are no unit tests covering these cases. There are existing NUnit tests for other converters under test/Twilio.Test/Converters; adding a focused test for DateTimeConverter would help prevent regressions across different token shapes (null, string, date token, array).

Copilot uses AI. Check for mistakes.
else if (reader.TokenType == JsonToken.EndArray)
{
return dateTimes;
Expand All @@ -58,6 +75,15 @@
throw new JsonSerializationException("Failed to deserialize DateTime.");
}

private static DateTime ConvertToDateTime(object value)
{
if (value is DateTime dt)
return dt;
if (value is DateTimeOffset dto)
return dto.DateTime;
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConvertToDateTime converts DateTimeOffset via dto.DateTime, which drops the offset and returns a DateTime with Kind=Unspecified. For non-zero offsets this changes the represented instant (e.g., +02:00 stays 10:30 instead of 08:30 UTC). Use dto.UtcDateTime (or explicitly document/implement the intended offset handling) so DateTimeOffset tokens deserialize consistently with the ISO-8601 "...Z" UTC semantics used elsewhere (e.g., Serializers.DateTimeIso8601).

Suggested change
return dto.DateTime;
return dto.UtcDateTime;

Copilot uses AI. Check for mistakes.
throw new JsonSerializationException($"Cannot convert {value?.GetType()} to DateTime.");
}

public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime?) || objectType ==typeof(List<DateTime>);
Expand Down
107 changes: 107 additions & 0 deletions test/Twilio.Test/Converters/DateTimeConverterTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using NUnit.Framework;
using Twilio.Converters;

namespace Twilio.Tests.Converters
{
[TestFixture]
public class DateTimeConverterTest
{
private class SingleDateModel
{
[JsonConverter(typeof(DateTimeConverter))]
public DateTime? Date { get; set; }
}

private class DateListModel
{
[JsonConverter(typeof(DateTimeConverter))]
public List<DateTime> Dates { get; set; }
}

[Test]
public void TestDeserializeNullToken()
{
var json = "{\"Date\": null}";
var result = JsonConvert.DeserializeObject<SingleDateModel>(json);
Assert.IsNull(result.Date);

Check warning on line 29 in test/Twilio.Test/Converters/DateTimeConverterTest.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Dereference of a possibly null reference.

See more on https://sonarcloud.io/project/issues?id=twilio_twilio-csharp&issues=AZ3Ps126vG-Y2rnWpw4A&open=AZ3Ps126vG-Y2rnWpw4A&pullRequest=827
}

[Test]
public void TestDeserializeDateToken()
{
var settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.DateTime };
var json = "{\"Date\": \"2024-06-15T10:30:00Z\"}";
var result = JsonConvert.DeserializeObject<SingleDateModel>(json, settings);
Assert.IsNotNull(result.Date);

Check warning on line 38 in test/Twilio.Test/Converters/DateTimeConverterTest.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Dereference of a possibly null reference.

See more on https://sonarcloud.io/project/issues?id=twilio_twilio-csharp&issues=AZ3Ps126vG-Y2rnWpw4B&open=AZ3Ps126vG-Y2rnWpw4B&pullRequest=827
Assert.AreEqual(2024, result.Date.Value.Year);

Check warning on line 39 in test/Twilio.Test/Converters/DateTimeConverterTest.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Nullable value type may be null.

See more on https://sonarcloud.io/project/issues?id=twilio_twilio-csharp&issues=AZ3Ps126vG-Y2rnWpw4C&open=AZ3Ps126vG-Y2rnWpw4C&pullRequest=827
Assert.AreEqual(6, result.Date.Value.Month);
Assert.AreEqual(15, result.Date.Value.Day);
}

[Test]
public void TestDeserializeDateTimeOffsetToken()
{
var settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.DateTimeOffset };
var json = "{\"Date\": \"2024-06-15T10:30:00Z\"}";
var result = JsonConvert.DeserializeObject<SingleDateModel>(json, settings);
Assert.IsNotNull(result.Date);

Check warning on line 50 in test/Twilio.Test/Converters/DateTimeConverterTest.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Dereference of a possibly null reference.

See more on https://sonarcloud.io/project/issues?id=twilio_twilio-csharp&issues=AZ3Ps126vG-Y2rnWpw4D&open=AZ3Ps126vG-Y2rnWpw4D&pullRequest=827
Assert.AreEqual(2024, result.Date.Value.Year);

Check warning on line 51 in test/Twilio.Test/Converters/DateTimeConverterTest.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Nullable value type may be null.

See more on https://sonarcloud.io/project/issues?id=twilio_twilio-csharp&issues=AZ3Ps126vG-Y2rnWpw4E&open=AZ3Ps126vG-Y2rnWpw4E&pullRequest=827
Assert.AreEqual(6, result.Date.Value.Month);
Assert.AreEqual(15, result.Date.Value.Day);
}
Comment on lines +45 to +54
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DateTimeOffset-path test only covers a "Z" (zero-offset) timestamp and doesn’t catch offset-loss bugs (e.g., +02:00 should be normalized/handled explicitly). Add a test case with a non-zero offset (and assert the expected hour/Kind) so the DateTimeOffset handling in the converter is validated.

Copilot uses AI. Check for mistakes.

[Test]
public void TestDeserializeStringToken()
{
var settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.None };
var json = "{\"Date\": \"2024-06-15T10:30:00Z\"}";
var result = JsonConvert.DeserializeObject<SingleDateModel>(json, settings);
Assert.IsNotNull(result.Date);

Check warning on line 62 in test/Twilio.Test/Converters/DateTimeConverterTest.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Dereference of a possibly null reference.

See more on https://sonarcloud.io/project/issues?id=twilio_twilio-csharp&issues=AZ3Ps126vG-Y2rnWpw4F&open=AZ3Ps126vG-Y2rnWpw4F&pullRequest=827
Assert.AreEqual(2024, result.Date.Value.Year);

Check warning on line 63 in test/Twilio.Test/Converters/DateTimeConverterTest.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Nullable value type may be null.

See more on https://sonarcloud.io/project/issues?id=twilio_twilio-csharp&issues=AZ3Ps126vG-Y2rnWpw4G&open=AZ3Ps126vG-Y2rnWpw4G&pullRequest=827
}

[Test]
public void TestDeserializeArrayOfDateStrings()
{
var settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.None };
var json = "{\"Dates\": [\"2024-01-01T00:00:00Z\", \"2024-12-31T00:00:00Z\"]}";
var result = JsonConvert.DeserializeObject<DateListModel>(json, settings);
Assert.AreEqual(2, result.Dates.Count);

Check warning on line 72 in test/Twilio.Test/Converters/DateTimeConverterTest.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Dereference of a possibly null reference.

See more on https://sonarcloud.io/project/issues?id=twilio_twilio-csharp&issues=AZ3Ps126vG-Y2rnWpw4H&open=AZ3Ps126vG-Y2rnWpw4H&pullRequest=827
Assert.AreEqual(1, result.Dates[0].Month);
Assert.AreEqual(12, result.Dates[1].Month);
}

[Test]
public void TestDeserializeArrayOfDateTokens()
{
var settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.DateTime };
var json = "{\"Dates\": [\"2024-01-01T00:00:00Z\", \"2024-12-31T00:00:00Z\"]}";
var result = JsonConvert.DeserializeObject<DateListModel>(json, settings);
Assert.AreEqual(2, result.Dates.Count);

Check warning on line 83 in test/Twilio.Test/Converters/DateTimeConverterTest.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Dereference of a possibly null reference.

See more on https://sonarcloud.io/project/issues?id=twilio_twilio-csharp&issues=AZ3Ps126vG-Y2rnWpw4I&open=AZ3Ps126vG-Y2rnWpw4I&pullRequest=827
Assert.AreEqual(1, result.Dates[0].Month);
Assert.AreEqual(12, result.Dates[1].Month);
}

[Test]
public void TestDeserializeArrayOfDateTimeOffsetTokens()
{
var settings = new JsonSerializerSettings { DateParseHandling = DateParseHandling.DateTimeOffset };
var json = "{\"Dates\": [\"2024-01-01T00:00:00Z\", \"2024-12-31T00:00:00Z\"]}";
var result = JsonConvert.DeserializeObject<DateListModel>(json, settings);
Assert.AreEqual(2, result.Dates.Count);

Check warning on line 94 in test/Twilio.Test/Converters/DateTimeConverterTest.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Dereference of a possibly null reference.

See more on https://sonarcloud.io/project/issues?id=twilio_twilio-csharp&issues=AZ3Ps126vG-Y2rnWpw4J&open=AZ3Ps126vG-Y2rnWpw4J&pullRequest=827
Assert.AreEqual(1, result.Dates[0].Month);
Assert.AreEqual(12, result.Dates[1].Month);
}

[Test]
public void TestDeserializeInvalidTokenThrows()
{
var json = "{\"Date\": 12345}";
Assert.Throws<JsonSerializationException>(() =>
JsonConvert.DeserializeObject<SingleDateModel>(json));
}
}
}
Loading