? StringComparer.OrdinalIgnoreCase
: StringComparer.Ordinal);
- HashSet<string>? ignoredProperties = null;
+ Dictionary<string, PropertyInfo>? ignoredProperties = null;
- // We start from the most derived type and ascend to the base type.
+ // We start from the most derived type.
for (Type? currentType = type; currentType != null; currentType = currentType.BaseType)
{
foreach (PropertyInfo propertyInfo in currentType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
{
- // Ignore indexers
- if (propertyInfo.GetIndexParameters().Length > 0)
+ // Ignore indexers and virtual properties that have overrides that were [JsonIgnore]d.
+ if (propertyInfo.GetIndexParameters().Length > 0 || PropertyIsOverridenAndIgnored(propertyInfo, ignoredProperties))
{
continue;
}
// Is the current property hidden by the previously cached property
// (with `new` keyword, or by overriding)?
other.PropertyInfo!.Name != propertyName &&
- // Was a property with the same CLR name was ignored? That property hid the current property,
+ // Was a property with the same CLR name ignored? That property hid the current property,
// thus, if it was ignored, the current property should be ignored too.
- ignoredProperties?.Contains(propertyName) != true)
+ ignoredProperties?.ContainsKey(propertyName) != true
+ )
{
- // We throw if we have two public properties that have the same JSON property name, and neither have been ignored.
+ // Throw if we have two public properties with the same JSON property name,
+ // neither overrides or hides the other, and neither have been ignored.
ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(Type, jsonPropertyInfo);
}
// Ignore the current property.
if (jsonPropertyInfo.IsIgnored)
{
- (ignoredProperties ??= new HashSet<string>()).Add(propertyName);
+ (ignoredProperties ??= new Dictionary<string, PropertyInfo>())[propertyName] = propertyInfo;
}
}
else
PropertyCache = propertyCache;
}
+ private static bool PropertyIsOverridenAndIgnored(PropertyInfo currentProperty, Dictionary<string, PropertyInfo>? ignoredProperties)
+ {
+ if (ignoredProperties == null || !ignoredProperties.TryGetValue(currentProperty.Name, out PropertyInfo? ignoredProperty))
+ {
+ return false;
+ }
+
+ return currentProperty.PropertyType == ignoredProperty.PropertyType &&
+ PropertyIsVirtual(currentProperty) &&
+ PropertyIsVirtual(ignoredProperty);
+ }
+
+ private static bool PropertyIsVirtual(PropertyInfo propertyInfo)
+ {
+ return propertyInfo.GetMethod?.IsVirtual == true || propertyInfo.SetMethod?.IsVirtual == true;
+ }
+
public bool DetermineExtensionDataProperty(Dictionary<string, JsonPropertyInfo> cache)
{
JsonPropertyInfo? jsonPropertyInfo = GetPropertyWithUniqueAttribute(Type, typeof(JsonExtensionDataAttribute), cache);
{
// Serialize
var obj = new ClassWithPropertyNamingConflict();
+
+ // Newtonsoft.Json throws JsonSerializationException here because
+ // non-public properties are included when [JsonProperty] is placed on them.
string json = JsonSerializer.Serialize(obj);
Assert.Equal(@"{""MyString"":""DefaultValue""}", json);
// Deserialize
json = @"{""MyString"":""NewValue""}";
+
+ // Newtonsoft.Json throws JsonSerializationException here because
+ // non-public properties are included when [JsonProperty] is placed on them.
obj = JsonSerializer.Deserialize<ClassWithPropertyNamingConflict>(json);
Assert.Equal("NewValue", obj.MyString);
}
[Fact]
- public static void Serealize_NewSlotPublicProperty_ConflictWithBasePublicProperty()
+ public static void Serialize_NewSlotPublicProperty_ConflictWithBasePublicProperty()
{
// Serialize
var obj = new ClassWithNewSlotDecimalProperty();
}
[Fact]
- public static void Serealize_NewSlotPublicProperty_SpecifiedJsonPropertyName()
+ public static void Serialize_NewSlotPublicProperty_SpecifiedJsonPropertyName()
{
// Serialize
var obj = new ClassWithNewSlotAttributedDecimalProperty();
Assert.Equal(@"{}", json);
+ // Newtonsoft.Json has the following output because non-public properties are included when [JsonProperty] is placed on them.
+ // {"MyString":"ConflictingValue"}
+
// Deserialize
json = @"{""MyString"":""NewValue""}";
obj = JsonSerializer.Deserialize<ClassWithIgnoredPropertyNamingConflictPrivate>(json);
Assert.Equal("DefaultValue", obj.MyString);
Assert.Equal("ConflictingValue", obj.ConflictingString);
+
+ // The output for Newtonsoft.Json is:
+ // obj.ConflictingString = "NewValue"
+ // obj.MyString still equals "DefaultValue"
}
[Fact]
Assert.Throws<InvalidOperationException>(
() => JsonSerializer.Serialize(obj));
+ // The output for Newtonsoft.Json is:
+ // {"MyString":"ConflictingValue"}
+ // Conflicts at different type-hierarchy levels that are not caused by
+ // deriving or the new keyword are allowed. Properties on more derived types win.
+
// Deserialize
string json = @"{""MyString"":""NewValue""}";
Assert.Throws<InvalidOperationException>(
() => JsonSerializer.Deserialize<ClassInheritedWithPropertyNamingConflictWhichThrows>(json));
+
+ // The output for Newtonsoft.Json is:
+ // obj.ConflictingString = "NewValue"
+ // obj.MyString still equals "DefaultValue"
}
[Fact]
Assert.Throws<InvalidOperationException>(
() => JsonSerializer.Serialize(obj));
+ // The output for Newtonsoft.Json is:
+ // {"MyString":"ConflictingValue"}
+ // Conflicts at different type-hierarchy levels that are not caused by
+ // deriving or the new keyword are allowed. Properties on more derived types win.
+
// Deserialize
string json = @"{""MyString"":""NewValue""}";
+
Assert.Throws<InvalidOperationException>(
() => JsonSerializer.Deserialize<ClassTwiceInheritedWithPropertyNamingConflictWhichThrows>(json));
+
+ // The output for Newtonsoft.Json is:
+ // obj.ConflictingString = "NewValue"
+ // obj.MyString still equals "DefaultValue"
}
[Fact]
// Serialize
var obj = new ClassInheritedWithPropertyPolicyConflictWhichThrows();
+
Assert.Throws<InvalidOperationException>(
() => JsonSerializer.Serialize(obj, options));
+ // The output for Newtonsoft.Json is:
+ // {"myString":"ConflictingValue"}
+ // Conflicts at different type-hierarchy levels that are not caused by
+ // deriving or the new keyword are allowed. Properties on more derived types win.
+
// Deserialize
string json = @"{""MyString"":""NewValue""}";
Assert.Throws<InvalidOperationException>(
() => JsonSerializer.Deserialize<ClassInheritedWithPropertyPolicyConflictWhichThrows>(json, options));
+
+ // The output for Newtonsoft.Json is:
+ // obj.myString = "NewValue"
+ // obj.MyString still equals "DefaultValue"
}
[Fact]
// Serialize
var obj = new ClassTwiceInheritedWithPropertyPolicyConflictWhichThrows();
+
Assert.Throws<InvalidOperationException>(
() => JsonSerializer.Serialize(obj, options));
+ // The output for Newtonsoft.Json is:
+ // {"myString":"ConflictingValue"}
+ // Conflicts at different type-hierarchy levels that are not caused by
+ // deriving or the new keyword are allowed. Properties on more derived types win.
+
// Deserialize
string json = @"{""MyString"":""NewValue""}";
+
Assert.Throws<InvalidOperationException>(
() => JsonSerializer.Deserialize<ClassTwiceInheritedWithPropertyPolicyConflictWhichThrows>(json, options));
+
+ // The output for Newtonsoft.Json is:
+ // obj.myString = "NewValue"
+ // obj.MyString still equals "DefaultValue"
}
[Fact]
- public static void HiddenPropertiesIgnored_WhenOverridesIgnored_AndPropertyNameConflicts()
+ public static void HiddenPropertiesIgnored_WhenOverridesIgnored()
{
string serialized = JsonSerializer.Serialize(new DerivedClass_With_IgnoredOverride());
+ Assert.Equal(@"{}", serialized);
+
+ serialized = JsonSerializer.Serialize(new DerivedClass_WithVisibleProperty_Of_DerivedClass_With_IgnoredOverride());
Assert.Equal(@"{""MyProp"":false}", serialized);
serialized = JsonSerializer.Serialize(new DerivedClass_With_IgnoredOverride_And_ConflictingPropertyName());
Assert.Equal(@"{""MyProp"":null}", serialized);
- serialized = JsonSerializer.Serialize(new DerivedClass_With_NewProperty());
+ serialized = JsonSerializer.Serialize(new DerivedClass_With_Ignored_NewProperty());
Assert.Equal(@"{""MyProp"":false}", serialized);
- serialized = JsonSerializer.Serialize(new DerivedClass_With_NewProperty_And_ConflictingPropertyName());
- Assert.Equal(@"{""MyProp"":null}", serialized);
+ serialized = JsonSerializer.Serialize(new DerivedClass_WithConflictingNewMember());
+ Assert.Equal(@"{""MyProp"":false}", serialized);
+
+ serialized = JsonSerializer.Serialize(new DerivedClass_WithConflictingNewMember_Of_DifferentType());
+ Assert.Equal(@"{""MyProp"":0}", serialized);
- serialized = JsonSerializer.Serialize(new DerivedClass_WithNewProperty_Of_DifferentType());
+ serialized = JsonSerializer.Serialize(new DerivedClass_With_Ignored_ConflictingNewMember());
Assert.Equal(@"{""MyProp"":false}", serialized);
- serialized = JsonSerializer.Serialize(new DerivedClass_WithNewProperty_Of_DifferentType_And_ConflictingPropertyName());
+ serialized = JsonSerializer.Serialize(new DerivedClass_With_Ignored_ConflictingNewMember_Of_DifferentType());
+ Assert.Equal(@"{""MyProp"":false}", serialized);
+
+ serialized = JsonSerializer.Serialize(new DerivedClass_With_NewProperty_And_ConflictingPropertyName());
Assert.Equal(@"{""MyProp"":null}", serialized);
- serialized = JsonSerializer.Serialize(new DerivedClass_WithIgnoredOverride());
+ serialized = JsonSerializer.Serialize(new DerivedClass_With_Ignored_NewProperty_Of_DifferentType());
Assert.Equal(@"{""MyProp"":false}", serialized);
+ serialized = JsonSerializer.Serialize(new DerivedClass_With_Ignored_NewProperty_Of_DifferentType_And_ConflictingPropertyName());
+ Assert.Equal(@"{""MyProp"":null}", serialized);
+
serialized = JsonSerializer.Serialize(new FurtherDerivedClass_With_ConflictingPropertyName());
Assert.Equal(@"{""MyProp"":null}", serialized);
+ // Here we differ from Newtonsoft.Json, where the output would be
+ // {"MyProp":null}
+ // Conflicts at different type-hierarchy levels that are not caused by
+ // deriving or the new keyword are allowed. Properties on more derived types win.
+ // This is invalid in System.Text.Json.
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new DerivedClass_WithConflictingPropertyName()));
serialized = JsonSerializer.Serialize(new FurtherDerivedClass_With_IgnoredOverride());
public override bool MyProp { get; set; }
}
+ private class DerivedClass_WithVisibleProperty_Of_DerivedClass_With_IgnoredOverride : DerivedClass_With_IgnoredOverride
+ {
+ public override bool MyProp { get; set; }
+ }
+
private class DerivedClass_With_IgnoredOverride_And_ConflictingPropertyName : Class_With_VirtualProperty
{
[JsonPropertyName("MyProp")]
public bool MyProp { get; set; }
}
- private class DerivedClass_With_NewProperty : Class_With_Property
+ private class DerivedClass_With_Ignored_NewProperty : Class_With_Property
{
[JsonIgnore]
public new bool MyProp { get; set; }
public new bool MyProp { get; set; }
}
- private class DerivedClass_WithNewProperty_Of_DifferentType : Class_With_Property
+ private class DerivedClass_With_Ignored_NewProperty_Of_DifferentType : Class_With_Property
{
[JsonIgnore]
public new int MyProp { get; set; }
}
- private class DerivedClass_WithNewProperty_Of_DifferentType_And_ConflictingPropertyName : Class_With_Property
+ private class DerivedClass_With_Ignored_NewProperty_Of_DifferentType_And_ConflictingPropertyName : Class_With_Property
{
[JsonPropertyName("MyProp")]
public string MyString { get; set; }
public override bool MyProp { get; set; }
}
+ private class DerivedClass_WithConflictingNewMember : Class_With_VirtualProperty
+ {
+ public new bool MyProp { get; set; }
+ }
+
+ private class DerivedClass_WithConflictingNewMember_Of_DifferentType : Class_With_VirtualProperty
+ {
+ public new int MyProp { get; set; }
+ }
+
+ private class DerivedClass_With_Ignored_ConflictingNewMember : Class_With_VirtualProperty
+ {
+ [JsonIgnore]
+ public new bool MyProp { get; set; }
+ }
+
+ private class DerivedClass_With_Ignored_ConflictingNewMember_Of_DifferentType : Class_With_VirtualProperty
+ {
+ [JsonIgnore]
+ public new int MyProp { get; set; }
+ }
+
private class FurtherDerivedClass_With_ConflictingPropertyName : DerivedClass_WithIgnoredOverride
{
[JsonPropertyName("MyProp")]