-
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
Fix NullReference on JsonExtensionData #34569
Conversation
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 for the PR. Just a few comments.
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs
Show resolved
Hide resolved
{ | ||
public override Dictionary<string, object> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
{ | ||
throw new NotImplementedException(); |
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.
Let's add a test for deserialization that ensures that the converter is not called.
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.
Prior to this PR the cache gets populated based on the converter that is specified by [JsonConverter]
or through JsonSerializerOptions
, but upon serialization/deserialization it is assuming that we have a JsonDictionaryConverter<>
. My PR makes sure for writing we do not make that assumption, but for reading it is more complicated, because we store only one converter per property. If we want different behaviors for read and write, we need to have the custom serializer registered for write and JsonDictionaryConverter<>
for read, at lease for the extension data.
Current code
JsonConverter<object> converter = (JsonConverter<object>)state.Current.JsonPropertyInfo!.RuntimeClassInfo.ElementClassInfo!.PropertyInfoForClassInfo.ConverterBase;
Assumes that ElementClassInfo
is not null
which is not correct in this case and currently it throws with NullReferenceException
so the suggested test would fail. I think better to fix that in a separate PR.
runtime/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs
Line 219 in c653291
JsonConverter<object> converter = (JsonConverter<object>) |
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.
We can mitigate the null ref by fetching the converter from the Options
instance, rather than ElementClassInfo
, similar to
runtime/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs
Line 265 in c653291
JsonConverter<JsonElement> converter = (JsonConverter<JsonElement>)Options.GetConverter(typeof(JsonElement)); |
We should make the change in this PR.
The other option is to assign ElementType
when handling ClassType.Value
types during warm up, similar to
runtime/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs
Line 170 in c653291
ElementType = converter.ElementType; |
We probably shouldn't go down this route since ElementType
and ElementClassInfo
are logically reserved for collections (de)serialized without custom converters.
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.
In light of this discussion, let's add and test two more classes for deserialization (where the extension data element type is JsonElement
), to ensure that the converter (another class to add) not called.
- a test for deserialization where the converter is added to the
options.Converters
collection - a test for deserialization where
JsonConverterAttribute
is placed on the property.
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.
Updated and tests are added
src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs
Show resolved
Hide resolved
@@ -217,7 +217,7 @@ public bool ReadJsonAndAddExtensionProperty(object obj, ref ReadStack state, ref | |||
else | |||
{ | |||
JsonConverter<object> converter = (JsonConverter<object>) | |||
state.Current.JsonPropertyInfo!.RuntimeClassInfo.ElementClassInfo!.PropertyInfoForClassInfo.ConverterBase; | |||
(state.Current.JsonPropertyInfo!.RuntimeClassInfo.ElementClassInfo?.PropertyInfoForClassInfo.ConverterBase ?? Options.GetConverter(typeof(object))); |
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.
For simplicity, this should just be
JsonConverter<object> converter = (JsonConverter<object>)Options.GetConverter(typeof(object)));
This change shouldn't have any noticeable perf impact.
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.
We need a similar change on line 237. The tests I suggest below will fail if this change isn't made.
JsonConverter<JsonElement> converter = (JsonConverter<JsonElement>)Options.GetConverter(typeof(JsonElement)));
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.
done :)
Assert.Equal("TestValue", ((JsonElement)obj.Overflow["TestKey"]).GetString()); | ||
} | ||
|
||
private class ClassWithJsonElementExtensionData |
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.
We need another class where the extension property doesn't have the converter attribute and a test (for serialization and deserialization) where the converter is added at runtime with options.Converter.Add
.
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.
For completeness, we should also add tests (serialization and deserialization) for a class where the extension property dictionary is a custom type, and the JsonConverterAttribute
is applied on the custom type itself.
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.
The new test covers them
ClassWithJsonElementExtensionData obj | ||
= JsonSerializer.Deserialize<ClassWithJsonElementExtensionData>(@"{""TestKey"":""TestValue""}"); | ||
|
||
Assert.Equal("TestValue", obj.Overflow["TestKey"].GetString()); |
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.
We should test serialization here (Assert.Throws
is fine).
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.
The new test covers both serialization and deserialization
@@ -285,8 +285,17 @@ internal bool TryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions opt | |||
|
|||
internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state) | |||
{ | |||
Debug.Assert(!(value is null)); |
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.
nit:
Debug.Assert(!(value is null)); | |
Debug.Assert(value != null); |
for consistency with the rest of the project.
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.
+1
@layomia I changed the tests to ensure we cover all the combinations:
|
Fix NullReferenceException on serialization when JsonExtensionData dictionary has a custom JsonConverter. Fix #32903
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!
It would be good to add a test where the converter is placed on the type itself as described in #34569 (comment). I'll pick this up in a follow up.
Test failure unrelated. |
Fix NullReferenceException on serialization when JsonExtensionData dictionary has a custom JsonConverter.
Fix #32903