There seems to be a change in .NET 5 with regard to JsonConverters and nullable types. Previously converter would not be called for null values.
This test fails on .NET 5, works fine previously.
public class SalesOrder
{
[JsonConverter(typeof(JsonMicrosoftDateTimeConverter))]
public DateTime? PaymentDueDate { get; set; }
}
[Fact]
public static void DeserialiseSalesOrderWithNullPaymentDueDateTest()
{
string jsonSalesOrder = @"{
""PaymentDueDate"": null
}";
// Exception thrown here on .NET 5.
var order = JsonSerializer.Deserialize<SalesOrder>(jsonSalesOrder);
Assert.NotNull(order);
Assert.Null(order.PaymentDueDate);
// Quick check to re-serialize process, to make sure we can go back and forth without issue
string reSerialized = JsonSerializer.Serialize(order);
Assert.NotNull(reSerialized);
}
public class JsonMicrosoftDateTimeConverter : JsonConverterFactory
{
/// <inheritdoc/>
public override bool CanConvert(Type typeToConvert)
{
// Don't perform a typeToConvert == null check for performance. Trust our callers will be nice.
return typeToConvert == typeof(DateTime)
|| (typeToConvert.IsGenericType && IsNullableDateTime(typeToConvert));
}
/// <inheritdoc/>
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
// Don't perform a typeToConvert == null check for performance. Trust our callers will be nice.
return typeToConvert.IsGenericType
? (JsonConverter)new JsonNullableDateTimeConverter()
: new JsonStandardDateTimeConverter();
}
private static bool IsNullableDateTime(Type typeToConvert)
{
Type UnderlyingType = Nullable.GetUnderlyingType(typeToConvert);
return UnderlyingType != null && UnderlyingType == typeof(DateTime);
}
internal class JsonStandardDateTimeConverter : JsonDateTimeConverter<DateTime>
{
/// <inheritdoc/>
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadDateTime(ref reader);
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
=> WriteDateTime(writer, value);
}
internal class JsonNullableDateTimeConverter : JsonDateTimeConverter<DateTime?>
{
/// <inheritdoc/>
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadDateTime(ref reader);
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
=> WriteDateTime(writer, value!.Value);
}
internal abstract class JsonDateTimeConverter<T> : JsonConverter<T>
{
private static readonly DateTime s_Epoch = DateTime.SpecifyKind(new DateTime(1970, 1, 1, 0, 0, 0), DateTimeKind.Utc);
private static readonly Regex s_Regex = new Regex("^/Date\\(([^+-]+)\\)/$", RegexOptions.CultureInvariant | RegexOptions.Compiled);
public static DateTime ReadDateTime(ref Utf8JsonReader reader)
{
if (reader.TokenType != JsonTokenType.String)
throw new JsonException();
string formatted = reader.GetString();
Match match = s_Regex.Match(formatted);
return !match.Success
|| !long.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out long unixTime)
? throw new JsonException("Unexpected value format, unable to parse DateTime.")
: s_Epoch.AddMilliseconds(unixTime);
}
public static void WriteDateTime(Utf8JsonWriter writer, DateTime value)
{
long unixTime = Convert.ToInt64((value.ToUniversalTime() - s_Epoch).TotalMilliseconds);
string formatted = FormattableString.Invariant($"/Date({unixTime})/");
writer.WriteStringValue(formatted);
}
}
}
Read method could detect Null token and handle it. But up until now it hasn't needed to. .NET 5 could potentially break a lot of JsonConverters reliant on this behavior.
It works in .NET Core 3 & 3.1. .NET Standard 2.0, 2.1.
https://github.com/Macross-Software/core/issues/7
/cc @gavinharriss
@layomia I thought we did this behind an opt-in: https://github.com/dotnet/runtime/issues/34439
@layomia I thought we did this behind an opt-in: #34439
That's the expected behavior. Will take a look.
@layomia Just checking in on this. Any update? Anything I can do to help?
@CodeBlanch thanks for reporting this issue. Yes there was an unintended change for null handling from 3.0 to 5.0 and the linked PR addresses it.
Most helpful comment
@CodeBlanch thanks for reporting this issue. Yes there was an unintended change for null handling from 3.0 to 5.0 and the linked PR addresses it.