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

Loosen property name collision detection involving hidden properties #36936

Merged
merged 3 commits into from
May 27, 2020

Conversation

layomia
Copy link
Contributor

@layomia layomia commented May 24, 2020

The change in #32107 allowed the (de)serialization of hidden properties when the properties in the derived classes are ignored/non-public.

As a result of the change, the serializer now examines properties at all levels of the inheritance chain, rather than just the ones visible from the type to be serialized. This means that InvalidOperationException could be thrown if there is a JSON property name conflict between inheritance levels, if the property higher in the inheritance chain was not hidden by the conflicting property in the more derived class (but by another property). A property is "hidden" if it is virtual and overridden in a derived class with polymorphism or with the new keyword.

The breaking change was flagged in ASP.NET Core preview 5 validation: dotnet/aspnetcore#22132.

To fix the break, this change loosens property name collision detection involving hidden properties. Rather than throw InvalidOperationException, the serializer will a ignore hidden property when the property that hid it is ignored and there's a property name conflict. This aligns with Newtonsoft.Json behavior.

@layomia layomia added this to the 5.0 milestone May 24, 2020
@layomia layomia self-assigned this May 24, 2020
{
// Check for name equality is required to determine when a new slot is used for the member.
// Therefore, if names are not the same, there is conflict due to the name policy or attributes.
// The collision is invalid if none of the following is true:
Copy link
Member

Choose a reason for hiding this comment

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

This is hard to follow. Can we just state the conditions when we do throw?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've simplified the comments.


if (jsonPropertyInfo.IsIgnored)
{
(ignoredProperties ??= new HashSet<string>()).Add(propertyName);
Copy link
Member

Choose a reason for hiding this comment

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

Does this have to respect the case-insensitive mode?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This cache tells us the CLR names for properties that have been ignored. This is used when looking at properties of a base type, and there's a conflict with a property at a derived type (which has a different CLR name, but the same JSON property name, due to JsonPropertyName or the naming policy).

If another property on a derived class has the same CLR name as the current property on the base type, we know that the former hid the latter. If the property on the derived class was ignored with JsonIgnore, we know to also ignore the current type being examined on the base type. This will mitigate the property name collision.

None of these considerations has to do with case-insensitivity.

cache[jsonPropertyInfo.NameAsString] = jsonPropertyInfo;
}
else if (other.PropertyInfo?.Name != jsonPropertyInfo.PropertyInfo?.Name &&
(jsonPropertyInfo.ShouldDeserialize == true || jsonPropertyInfo.ShouldSerialize == true))
else if (!(isIgnored || other.PropertyInfo!.Name == propertyName || ignoredProperties?.Contains(propertyName) == true))
Copy link
Member

Choose a reason for hiding this comment

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

is other.PropertyInfo!.Name == propertyName protecting against duplicate properties via [JsonPropertyName] and\or duplicate properties by casing (when using case-insensitive options?). Please add a comment around this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

other.PropertyInfo!.Name == propertyName indicates that the previously cached property ("other") hides the current property (either with the new keyword, or by overriding it). We shouldn't throw in this case, as it is likely for the two properties result in the same property name (since they have the same CLR name). Rather, we should ignore the current property.

I've added a comment.

Copy link
Member

@steveharter steveharter left a comment

Choose a reason for hiding this comment

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

Per offline review\discussion, these changes look good.

Notes:

  • [JsonPropertyName] does not impact "hiding". A base property that has the attribute (with name "foo") will not hide a more derived type with a "foo" CLR property -- instead it throws InvalidOperation. The same applies if the derived type has [JsonPropertyName] and a base property also has a "foo" CLR property.
  • A derived type hides a base type property of the same name (return type doesn't matter). The name can be case-insensitive if the JsonSerializerOptions are set for that.

@layomia layomia merged commit 6eae0e6 into dotnet:master May 27, 2020
layomia added a commit to layomia/dotnet_runtime that referenced this pull request May 28, 2020
…otnet#36936)

* Loosen property name collision detection involving hidden properties

* Delay ignored prop cache creation; add more tests

* Clarify comments
@layomia layomia deleted the collisions branch May 28, 2020 02:54
layomia added a commit that referenced this pull request May 28, 2020
…36936) (#37105)

* Loosen property name collision detection involving hidden properties

* Delay ignored prop cache creation; add more tests

* Clarify comments
@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.

4 participants