Skip to content

Commit 4ae4e2f

Browse files
authored
Loosen property name collision detection involving hidden properties (#36936) (#37105)
* Loosen property name collision detection involving hidden properties * Delay ignored prop cache creation; add more tests * Clarify comments
1 parent 201841e commit 4ae4e2f

File tree

2 files changed

+146
-27
lines changed

2 files changed

+146
-27
lines changed

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ public JsonClassInfo(Type type, JsonSerializerOptions options)
101101
? StringComparer.OrdinalIgnoreCase
102102
: StringComparer.Ordinal);
103103

104+
HashSet<string>? ignoredProperties = null;
105+
106+
// We start from the most derived type and ascend to the base type.
104107
for (Type? currentType = type; currentType != null; currentType = currentType.BaseType)
105108
{
106109
foreach (PropertyInfo propertyInfo in currentType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
@@ -111,44 +114,55 @@ public JsonClassInfo(Type type, JsonSerializerOptions options)
111114
continue;
112115
}
113116

114-
if (IsNonPublicProperty(propertyInfo))
115-
{
116-
if (JsonPropertyInfo.GetAttribute<JsonIncludeAttribute>(propertyInfo) != null)
117-
{
118-
ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(propertyInfo, currentType);
119-
}
120-
121-
// Non-public properties should not be included for (de)serialization.
122-
continue;
123-
}
124-
125-
// For now we only support public getters\setters
117+
// For now we only support public properties (i.e. setter and/or getter is public).
126118
if (propertyInfo.GetMethod?.IsPublic == true ||
127119
propertyInfo.SetMethod?.IsPublic == true)
128120
{
129121
JsonPropertyInfo jsonPropertyInfo = AddProperty(propertyInfo, currentType, options);
130122
Debug.Assert(jsonPropertyInfo != null && jsonPropertyInfo.NameAsString != null);
131123

132-
// If the JsonPropertyNameAttribute or naming policy results in collisions, throw an exception.
124+
string propertyName = propertyInfo.Name;
125+
126+
// The JsonPropertyNameAttribute or naming policy resulted in a collision.
133127
if (!JsonHelpers.TryAdd(cache, jsonPropertyInfo.NameAsString, jsonPropertyInfo))
134128
{
135129
JsonPropertyInfo other = cache[jsonPropertyInfo.NameAsString];
136130

137-
if (other.ShouldDeserialize == false && other.ShouldSerialize == false)
131+
if (other.IsIgnored)
138132
{
139-
// Overwrite the one just added since it has [JsonIgnore].
133+
// Overwrite previously cached property since it has [JsonIgnore].
140134
cache[jsonPropertyInfo.NameAsString] = jsonPropertyInfo;
141135
}
142-
else if (other.PropertyInfo?.Name != jsonPropertyInfo.PropertyInfo?.Name &&
143-
(jsonPropertyInfo.ShouldDeserialize == true || jsonPropertyInfo.ShouldSerialize == true))
136+
else if (
137+
// Does the current property have `JsonIgnoreAttribute`?
138+
!jsonPropertyInfo.IsIgnored &&
139+
// Is the current property hidden by the previously cached property
140+
// (with `new` keyword, or by overriding)?
141+
other.PropertyInfo!.Name != propertyName &&
142+
// Was a property with the same CLR name was ignored? That property hid the current property,
143+
// thus, if it was ignored, the current property should be ignored too.
144+
ignoredProperties?.Contains(propertyName) != true)
144145
{
145-
// Check for name equality is required to determine when a new slot is used for the member.
146-
// Therefore, if names are not the same, there is conflict due to the name policy or attributes.
146+
// We throw if we have two public properties that have the same JSON property name, and neither have been ignored.
147147
ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(Type, jsonPropertyInfo);
148148
}
149-
// else ignore jsonPropertyInfo since it has [JsonIgnore] or it's hidden by a new slot.
149+
// Ignore the current property.
150+
}
151+
152+
if (jsonPropertyInfo.IsIgnored)
153+
{
154+
(ignoredProperties ??= new HashSet<string>()).Add(propertyName);
150155
}
151156
}
157+
else
158+
{
159+
if (JsonPropertyInfo.GetAttribute<JsonIncludeAttribute>(propertyInfo) != null)
160+
{
161+
ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(propertyInfo, currentType);
162+
}
163+
164+
// Non-public properties should not be included for (de)serialization.
165+
}
152166
}
153167
}
154168

@@ -211,13 +225,6 @@ public JsonClassInfo(Type type, JsonSerializerOptions options)
211225
}
212226
}
213227

214-
private static bool IsNonPublicProperty(PropertyInfo propertyInfo)
215-
{
216-
MethodInfo? getMethod = propertyInfo.GetMethod;
217-
MethodInfo? setMethod = propertyInfo.SetMethod;
218-
return !((getMethod != null && getMethod.IsPublic) || (setMethod != null && setMethod.IsPublic));
219-
}
220-
221228
private void InitializeConstructorParameters(Dictionary<string, JsonPropertyInfo> propertyCache, ConstructorInfo constructorInfo)
222229
{
223230
ParameterInfo[] parameters = constructorInfo!.GetParameters();

src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,39 @@ public static void Throw_PublicProperty_ConflictDuePolicy_DobuleInheritance()
327327
() => JsonSerializer.Deserialize<ClassTwiceInheritedWithPropertyPolicyConflictWhichThrows>(json, options));
328328
}
329329

330+
[Fact]
331+
public static void HiddenPropertiesIgnored_WhenOverridesIgnored_AndPropertyNameConflicts()
332+
{
333+
string serialized = JsonSerializer.Serialize(new DerivedClass_With_IgnoredOverride());
334+
Assert.Equal(@"{""MyProp"":false}", serialized);
335+
336+
serialized = JsonSerializer.Serialize(new DerivedClass_With_IgnoredOverride_And_ConflictingPropertyName());
337+
Assert.Equal(@"{""MyProp"":null}", serialized);
338+
339+
serialized = JsonSerializer.Serialize(new DerivedClass_With_NewProperty());
340+
Assert.Equal(@"{""MyProp"":false}", serialized);
341+
342+
serialized = JsonSerializer.Serialize(new DerivedClass_With_NewProperty_And_ConflictingPropertyName());
343+
Assert.Equal(@"{""MyProp"":null}", serialized);
344+
345+
serialized = JsonSerializer.Serialize(new DerivedClass_WithNewProperty_Of_DifferentType());
346+
Assert.Equal(@"{""MyProp"":false}", serialized);
347+
348+
serialized = JsonSerializer.Serialize(new DerivedClass_WithNewProperty_Of_DifferentType_And_ConflictingPropertyName());
349+
Assert.Equal(@"{""MyProp"":null}", serialized);
350+
351+
serialized = JsonSerializer.Serialize(new DerivedClass_WithIgnoredOverride());
352+
Assert.Equal(@"{""MyProp"":false}", serialized);
353+
354+
serialized = JsonSerializer.Serialize(new FurtherDerivedClass_With_ConflictingPropertyName());
355+
Assert.Equal(@"{""MyProp"":null}", serialized);
356+
357+
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new DerivedClass_WithConflictingPropertyName()));
358+
359+
serialized = JsonSerializer.Serialize(new FurtherDerivedClass_With_IgnoredOverride());
360+
Assert.Equal(@"{""MyProp"":null}", serialized);
361+
}
362+
330363
public class ClassWithInternalProperty
331364
{
332365
internal string MyString { get; set; } = "DefaultValue";
@@ -474,6 +507,85 @@ public class ClassWithNewSlotAttributedDecimalProperty : ClassWithHiddenByNewSlo
474507
public new decimal MyNumeric { get; set; } = 1.5M;
475508
}
476509

510+
private class Class_With_VirtualProperty
511+
{
512+
public virtual bool MyProp { get; set; }
513+
}
514+
515+
private class DerivedClass_With_IgnoredOverride : Class_With_VirtualProperty
516+
{
517+
[JsonIgnore]
518+
public override bool MyProp { get; set; }
519+
}
520+
521+
private class DerivedClass_With_IgnoredOverride_And_ConflictingPropertyName : Class_With_VirtualProperty
522+
{
523+
[JsonPropertyName("MyProp")]
524+
public string MyString { get; set; }
525+
526+
[JsonIgnore]
527+
public override bool MyProp { get; set; }
528+
}
529+
530+
private class Class_With_Property
531+
{
532+
public bool MyProp { get; set; }
533+
}
534+
535+
private class DerivedClass_With_NewProperty : Class_With_Property
536+
{
537+
[JsonIgnore]
538+
public new bool MyProp { get; set; }
539+
}
540+
541+
private class DerivedClass_With_NewProperty_And_ConflictingPropertyName : Class_With_Property
542+
{
543+
[JsonPropertyName("MyProp")]
544+
public string MyString { get; set; }
545+
546+
[JsonIgnore]
547+
public new bool MyProp { get; set; }
548+
}
549+
550+
private class DerivedClass_WithNewProperty_Of_DifferentType : Class_With_Property
551+
{
552+
[JsonIgnore]
553+
public new int MyProp { get; set; }
554+
}
555+
556+
private class DerivedClass_WithNewProperty_Of_DifferentType_And_ConflictingPropertyName : Class_With_Property
557+
{
558+
[JsonPropertyName("MyProp")]
559+
public string MyString { get; set; }
560+
561+
[JsonIgnore]
562+
public new int MyProp { get; set; }
563+
}
564+
565+
private class DerivedClass_WithIgnoredOverride : Class_With_VirtualProperty
566+
{
567+
[JsonIgnore]
568+
public override bool MyProp { get; set; }
569+
}
570+
571+
private class FurtherDerivedClass_With_ConflictingPropertyName : DerivedClass_WithIgnoredOverride
572+
{
573+
[JsonPropertyName("MyProp")]
574+
public string MyString { get; set; }
575+
}
576+
577+
private class DerivedClass_WithConflictingPropertyName : Class_With_VirtualProperty
578+
{
579+
[JsonPropertyName("MyProp")]
580+
public string MyString { get; set; }
581+
}
582+
583+
private class FurtherDerivedClass_With_IgnoredOverride : DerivedClass_WithConflictingPropertyName
584+
{
585+
[JsonIgnore]
586+
public override bool MyProp { get; set; }
587+
}
588+
477589
[Fact]
478590
public static void NoSetter()
479591
{

0 commit comments

Comments
 (0)