Skip to content
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

Merged
merged 4 commits into from
Apr 14, 2020
Merged

Fix NullReference on JsonExtensionData #34569

merged 4 commits into from
Apr 14, 2020

Conversation

RezaJooyandeh
Copy link
Contributor

Fix NullReferenceException on serialization when JsonExtensionData dictionary has a custom JsonConverter.

Fix #32903

Copy link
Contributor

@layomia layomia left a 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.

{
public override Dictionary<string, object> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
Copy link
Contributor

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.

Copy link
Contributor Author

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.

JsonConverter<object> converter = (JsonConverter<object>)

Copy link
Contributor

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

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

.

We probably shouldn't go down this route since ElementType and ElementClassInfo are logically reserved for collections (de)serialized without custom converters.

Copy link
Contributor

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.

Copy link
Contributor Author

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

@layomia layomia added this to the 5.0 milestone Apr 7, 2020
@RezaJooyandeh RezaJooyandeh requested a review from layomia April 12, 2020 15:59
@@ -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)));
Copy link
Contributor

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.

Copy link
Contributor

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)));

Copy link
Contributor Author

@RezaJooyandeh RezaJooyandeh Apr 14, 2020

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
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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());
Copy link
Contributor

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).

Copy link
Contributor Author

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));
Copy link
Contributor

@layomia layomia Apr 13, 2020

Choose a reason for hiding this comment

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

nit:

Suggested change
Debug.Assert(!(value is null));
Debug.Assert(value != null);

for consistency with the rest of the project.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

+1

@RezaJooyandeh
Copy link
Contributor Author

RezaJooyandeh commented Apr 14, 2020

@layomia I changed the tests to ensure we cover all the combinations:

{Serialization|Deserialization}
{IDictionary<string, object|JsonElement>}
{Dictionary|CustomDictionary}
{Explict|Using options}

Copy link
Contributor

@layomia layomia left a 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.

@layomia
Copy link
Contributor

layomia commented Apr 14, 2020

Test failure unrelated.

@layomia layomia merged commit cfb109b into dotnet:master Apr 14, 2020
@RezaJooyandeh RezaJooyandeh deleted the issue-32903 branch April 15, 2020 00:46
@ghost ghost locked as resolved and limited conversation to collaborators Dec 9, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

NullReference when JsonExtensionData dictionary have a custom JsonConverter
3 participants