-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Honor converters for underlying types of Nullable<T> specified with JsonConverterAttribute #32006
Honor converters for underlying types of Nullable<T> specified with JsonConverterAttribute #32006
Conversation
…ullable<T> when converter can handle T.
@layomia is there an issue for this? |
...ries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.Int32.cs
Outdated
Show resolved
Hide resolved
@steveharter FYI I synced up and tested against your latest changes, fix seems to still be needed. |
Yes after I added that comment a few minutes later I updated it to reflect that it wouldn't be fixed. We should take your fix IMO. I'd like to here from one of the other reviewers as well. |
So the fix is about fallingback on the That doesn't sounds very intuitive and would cause a silent breaking change on the hierarchy that we currently have for converter registration https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#converter-registration-precedence Consider the following example: // Only allows numbers; otherwise throws.
private class MyFirstIntConverter : JsonConverter<int>
{
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Number)
{
return reader.GetInt32();
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
{
writer.WriteNumberValue(value);
}
}
// Allows numbers and strings; otherwise throws.
private class MySecondIntConverter : JsonConverter<int>
{
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Number)
{
return reader.GetInt32();
}
else if (reader.TokenType == JsonTokenType.String)
{
return Convert.ToInt32(reader.GetString());
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
{
writer.WriteNumberValue(value);
}
}
private class Int32Class
{
// Register the first converter on the property.
[JsonConverter(typeof(MyFirstIntConverter))]
public int? MyInt { get; set; }
}
public void TestConverterOnNullable()
{
var options = new JsonSerializerOptions();
// Register the second converter on the global options.
options.Converters.Add(new MySecondIntConverter());
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<int?>(@"{""MyInt"":""100000""}", options));
Assert.True(false, "You are not supposed to reach this");
} Instead, we should avoid throwing when looking at the |
@jozkee Interesting! No, it shouldn't be falling back to the options.Converters. I think something broke in @steveharter's refactor. It used to flow back out to: runtime/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs Lines 546 to 551 in 7c38fc9
Which would then retry for the underlying type and pull the correct converter off the attribute. I took a stab at fixing it but there's a lot of changes to digest. @steveharter Any ideas? I'm going to change the test to use a converter for a made-up type so it doesn't fall back to one of the built-in converts and succeed when it should fail (like it's doing now). |
Update... looks like the second pass using the default converts was actually intentional in the new design. |
...ries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, thanks. A couple comments; then we can take this PR
- Can you move your tests to a new
CustomConverterTests.NullableTypes.cs
file? The current locations don't seem specific. - Can you add tests for @jozkee's example above, showing that a converter handling the underlying type placed on a nullable type or property wins over one added at runtime?
Running CI again and updating the PR name to reflect the fix. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, @CodeBlanch!
Let's say we have a converter
Int32Converter
which knows how to converttypeof(int)
.This works:
But this doesn't work:
How the code works is it will first attempt the declared type. If it can't find a converter, and if it's a Nullable type, it will attempt to find a converter for the underlying type. But there's an exception thrown in the attribute case on the first pass that prevents the second phase from working.
This bug makes it a real pain to support Nullable in converters. Example, see: JsonTimeSpanConverter
To make it work today you have to handle Nullable in CanConvert & CreateConverter even though the engine won't call the converter for null. Fixing the bug enables all converters to also be Nullable converters, for free.