diff --git a/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs b/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs
index 53dcd2cfe71a27..87a0d2b95c96b4 100644
--- a/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs
+++ b/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs
@@ -324,5 +324,111 @@ public static bool TryGetDeserializationConstructor(
return defaultValue;
}
+
+ ///
+ /// Returns the type hierarchy for the given type, starting from the current type up to the base type(s) in the hierarchy.
+ /// Interface hierarchies with multiple inheritance will return results using topological sorting.
+ ///
+ public static Type[] GetSortedTypeHierarchy(
+#if !BUILDING_SOURCE_GENERATOR
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]
+#endif
+ this Type type)
+ {
+ if (!type.IsInterface)
+ {
+ // Non-interface hierarchies are linear, just walk up to the earliest ancestor.
+
+ var results = new List();
+ for (Type? current = type; current != null; current = current.BaseType)
+ {
+ results.Add(current);
+ }
+
+ return results.ToArray();
+ }
+ else
+ {
+ // Interface hierarchies support multiple inheritance,
+ // query the entire list and sort them topologically.
+ Type[] interfaces = type.GetInterfaces();
+ {
+ // include the current type into the list of interfaces
+ Type[] newArray = new Type[interfaces.Length + 1];
+ newArray[0] = type;
+ interfaces.CopyTo(newArray, 1);
+ interfaces = newArray;
+ }
+
+ TopologicalSort(interfaces, static (t1, t2) => t1.IsAssignableFrom(t2));
+ return interfaces;
+ }
+ }
+
+ private static void TopologicalSort(T[] inputs, Func isLessThan)
+ where T : notnull
+ {
+ // Standard implementation of in-place topological sorting using Kahn's algorithm.
+
+ if (inputs.Length < 2)
+ {
+ return;
+ }
+
+ var graph = new Dictionary>();
+ var next = new Queue();
+
+ // Step 1: construct the dependency graph.
+ for (int i = 0; i < inputs.Length; i++)
+ {
+ T current = inputs[i];
+ HashSet? dependencies = null;
+
+ for (int j = 0; j < inputs.Length; j++)
+ {
+ if (i != j && isLessThan(current, inputs[j]))
+ {
+ (dependencies ??= new()).Add(inputs[j]);
+ }
+ }
+
+ if (dependencies is null)
+ {
+ next.Enqueue(current);
+ }
+ else
+ {
+ graph.Add(current, dependencies);
+ }
+ }
+
+ Debug.Assert(next.Count > 0, "Input graph must be a DAG.");
+ int index = 0;
+
+ // Step 2: Walk the dependency graph starting with nodes that have no dependencies.
+ do
+ {
+ T nextTopLevelDependency = next.Dequeue();
+
+ foreach (KeyValuePair> kvp in graph)
+ {
+ HashSet dependencies = kvp.Value;
+ if (dependencies.Count > 0)
+ {
+ dependencies.Remove(nextTopLevelDependency);
+
+ if (dependencies.Count == 0)
+ {
+ next.Enqueue(kvp.Key);
+ }
+ }
+ }
+
+ inputs[index++] = nextTopLevelDependency;
+ }
+ while (next.Count > 0);
+
+ Debug.Assert(index == inputs.Length);
+ }
}
}
diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
index cbb6e5b0ad0473..8d1f090093595d 100644
--- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
+++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
@@ -1026,7 +1026,8 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener
bool propertyOrderSpecified = false;
- for (Type? currentType = type; currentType != null; currentType = currentType.BaseType)
+ // Walk the type hierarchy starting from the current type up to the base type(s)
+ foreach (Type currentType in type.GetSortedTypeHierarchy())
{
PropertyGenerationSpec spec;
@@ -1260,6 +1261,7 @@ private PropertyGenerationSpec GetPropertyGenerationSpec(
IsExtensionData = isExtensionData,
TypeGenerationSpec = GetOrAddTypeGenerationSpec(memberCLRType, generationMode),
DeclaringTypeRef = memberInfo.DeclaringType.GetCompilableName(),
+ DeclaringType = memberInfo.DeclaringType,
ConverterInstantiationLogic = converterInstantiationLogic,
HasFactoryConverter = hasFactoryConverter
};
diff --git a/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs b/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs
index 5417f25b6c649a..c885f4219c9452 100644
--- a/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs
+++ b/src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs
@@ -110,6 +110,8 @@ internal sealed class PropertyGenerationSpec
///
public string DeclaringTypeRef { get; init; }
+ public Type DeclaringType { get; init; }
+
///
/// Source code to instantiate design-time specified custom converter.
///
diff --git a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs
index 770e45d395b235..08032264b725f8 100644
--- a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs
+++ b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs
@@ -166,6 +166,7 @@ public bool TryFilterSerializableProps(
[NotNullWhen(true)] out Dictionary? serializableProperties,
out bool castingRequiredForProps)
{
+ castingRequiredForProps = false;
serializableProperties = new Dictionary();
Dictionary? ignoredMembers = null;
@@ -198,6 +199,10 @@ public bool TryFilterSerializableProps(
continue;
}
+ // Using properties from an interface hierarchy -- require explicit casting when
+ // getting properties in the fast path to account for possible diamond ambiguities.
+ castingRequiredForProps |= Type.IsInterface && propGenSpec.DeclaringType != Type;
+
string memberName = propGenSpec.ClrName!;
// The JsonPropertyNameAttribute or naming policy resulted in a collision.
@@ -210,32 +215,52 @@ public bool TryFilterSerializableProps(
// Overwrite previously cached property since it has [JsonIgnore].
serializableProperties[propGenSpec.RuntimePropertyName] = propGenSpec;
}
- else if (
- // Does the current property have `JsonIgnoreAttribute`?
- propGenSpec.DefaultIgnoreCondition != JsonIgnoreCondition.Always &&
- // Is the current property hidden by the previously cached property
- // (with `new` keyword, or by overriding)?
- other.ClrName != memberName &&
- // Was a property with the same CLR name was ignored? That property hid the current property,
- // thus, if it was ignored, the current property should be ignored too.
- ignoredMembers?.ContainsKey(memberName) != true)
+ else
{
- // We throw if we have two public properties that have the same JSON property name, and neither have been ignored.
- serializableProperties = null;
- castingRequiredForProps = false;
- return false;
+ bool ignoreCurrentProperty;
+
+ if (!Type.IsInterface)
+ {
+ ignoreCurrentProperty =
+ // Does the current property have `JsonIgnoreAttribute`?
+ propGenSpec.DefaultIgnoreCondition == JsonIgnoreCondition.Always ||
+ // Is the current property hidden by the previously cached property
+ // (with `new` keyword, or by overriding)?
+ other.ClrName == memberName ||
+ // 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.
+ ignoredMembers?.ContainsKey(memberName) == true;
+ }
+ else
+ {
+ // Unlike classes, interface hierarchies reject all naming conflicts for non-ignored properties.
+ // Conflicts like this are possible in two cases:
+ // 1. Diamond ambiguity in property names, or
+ // 2. Linear interface hierarchies that use properties with DIMs.
+ //
+ // Diamond ambiguities are not supported. Assuming there is demand, we might consider
+ // adding support for DIMs in the future, however that would require adding more APIs
+ // for the case of source gen.
+
+ ignoreCurrentProperty = propGenSpec.DefaultIgnoreCondition == JsonIgnoreCondition.Always;
+ }
+
+ if (!ignoreCurrentProperty)
+ {
+ // We have a conflict, emit a stub method that throws.
+ goto ReturnFalse;
+ }
}
- // Ignore the current property.
}
if (propGenSpec.DefaultIgnoreCondition == JsonIgnoreCondition.Always)
{
- (ignoredMembers ??= new Dictionary()).Add(memberName, propGenSpec);
+ (ignoredMembers ??= new()).Add(memberName, propGenSpec);
}
}
Debug.Assert(PropertyGenSpecList.Count >= serializableProperties.Count);
- castingRequiredForProps = PropertyGenSpecList.Count > serializableProperties.Count;
+ castingRequiredForProps |= PropertyGenSpecList.Count > serializableProperties.Count;
return true;
ReturnFalse:
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs
index e0937877f6c044..48a32b2a4a2dce 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs
@@ -791,25 +791,46 @@ internal void CacheMember(JsonPropertyInfo jsonPropertyInfo, JsonPropertyDiction
// Overwrite previously cached property since it has [JsonIgnore].
propertyCache[jsonPropertyInfo.Name] = jsonPropertyInfo;
}
- else if (
- // Does the current property have `JsonIgnoreAttribute`?
- !jsonPropertyInfo.IsIgnored &&
- // Is the current property hidden by the previously cached property
- // (with `new` keyword, or by overriding)?
- other.MemberName != memberName &&
- // Was a property with the same CLR name was ignored? That property hid the current property,
- // thus, if it was ignored, the current property should be ignored too.
- ignoredMembers?.ContainsKey(memberName) != true)
+ else
{
- // We throw if we have two public properties that have the same JSON property name, and neither have been ignored.
- ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(Type, jsonPropertyInfo.Name);
+ bool ignoreCurrentProperty;
+
+ if (!Type.IsInterface)
+ {
+ ignoreCurrentProperty =
+ // Does the current property have `JsonIgnoreAttribute`?
+ jsonPropertyInfo.IsIgnored ||
+ // Is the current property hidden by the previously cached property
+ // (with `new` keyword, or by overriding)?
+ other.MemberName == memberName ||
+ // 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.
+ ignoredMembers?.ContainsKey(memberName) == true;
+ }
+ else
+ {
+ // Unlike classes, interface hierarchies reject all naming conflicts for non-ignored properties.
+ // Conflicts like this are possible in two cases:
+ // 1. Diamond ambiguity in property names, or
+ // 2. Linear interface hierarchies that use properties with DIMs.
+ //
+ // Diamond ambiguities are not supported. Assuming there is demand, we might consider
+ // adding support for DIMs in the future, however that would require adding more APIs
+ // for the case of source gen.
+
+ ignoreCurrentProperty = jsonPropertyInfo.IsIgnored;
+ }
+
+ if (!ignoreCurrentProperty)
+ {
+ ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(Type, jsonPropertyInfo.Name);
+ }
}
- // Ignore the current property.
}
if (jsonPropertyInfo.IsIgnored)
{
- (ignoredMembers ??= new Dictionary()).Add(memberName, jsonPropertyInfo);
+ (ignoredMembers ??= new()).Add(memberName, jsonPropertyInfo);
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs
index 9e2fb80b3bd96d..fee942254b8015 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs
@@ -32,7 +32,7 @@ internal ReflectionJsonTypeInfo(JsonConverter converter, JsonSerializerOptions o
converter.ConfigureJsonTypeInfoUsingReflection(this, options);
}
- [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:RequiresUnreferencedCode",
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072:UnrecognizedReflectionPattern",
Justification = "The ctor is marked RequiresUnreferencedCode.")]
internal override void LateAddProperties()
{
@@ -44,104 +44,129 @@ internal override void LateAddProperties()
return;
}
- const BindingFlags BindingFlags =
- BindingFlags.Instance |
- BindingFlags.Public |
- BindingFlags.NonPublic |
- BindingFlags.DeclaredOnly;
-
- Dictionary? ignoredMembers = null;
- bool propertyOrderSpecified = false;
// Compiler adds RequiredMemberAttribute to type if any of the members is marked with 'required' keyword.
// SetsRequiredMembersAttribute means that all required members are assigned by constructor and therefore there is no enforcement
bool shouldCheckMembersForRequiredMemberAttribute =
typeof(T).HasRequiredMemberAttribute()
&& !(Converter.ConstructorInfo?.HasSetsRequiredMembersAttribute() ?? false);
- // Walk through the inheritance hierarchy, starting from the most derived type upward.
- for (Type? currentType = Type; currentType != null; currentType = currentType.BaseType)
+ bool propertyOrderSpecified = false;
+ Dictionary? ignoredMembers = null;
+
+ // Walk the type hierarchy starting from the current type up to the base type(s)
+ foreach (Type currentType in Type.GetSortedTypeHierarchy())
{
- PropertyInfo[] properties = currentType.GetProperties(BindingFlags);
+ if (currentType == ObjectType)
+ {
+ // Don't process any members for typeof(object)
+ break;
+ }
+
+ AddMembersDeclaredBySuperType(
+ currentType,
+ shouldCheckMembersForRequiredMemberAttribute,
+ ref propertyOrderSpecified,
+ ref ignoredMembers);
+ }
+
+ Debug.Assert(PropertyCache != null);
+
+ if (propertyOrderSpecified)
+ {
+ PropertyCache.List.StableSortByKey(static p => p.Value.Order);
+ }
+ }
+
+ [UnconditionalSuppressMessage("Trimming", "IL2070:UnrecognizedReflectionPattern",
+ Justification = "The ctor is marked RequiresUnreferencedCode.")]
+ private void AddMembersDeclaredBySuperType(
+ Type currentType,
+ bool shouldCheckMembersForRequiredMemberAttribute,
+ ref bool propertyOrderSpecified,
+ ref Dictionary? ignoredMembers)
+ {
+ Debug.Assert(!IsConfigured);
+ Debug.Assert(currentType.IsAssignableFrom(Type));
+
+ const BindingFlags BindingFlags =
+ BindingFlags.Instance |
+ BindingFlags.Public |
+ BindingFlags.NonPublic |
+ BindingFlags.DeclaredOnly;
+
+ PropertyInfo[] properties = currentType.GetProperties(BindingFlags);
- // PropertyCache is not accessed by other threads until the current JsonTypeInfo instance
- // is finished initializing and added to the cache in JsonSerializerOptions.
- // Default 'capacity' to the common non-polymorphic + property case.
- PropertyCache ??= CreatePropertyCache(capacity: properties.Length);
+ // PropertyCache is not accessed by other threads until the current JsonTypeInfo instance
+ // is finished initializing and added to the cache in JsonSerializerOptions.
+ // Default 'capacity' to the common non-polymorphic + property case.
+ PropertyCache ??= CreatePropertyCache(capacity: properties.Length);
- foreach (PropertyInfo propertyInfo in properties)
+ foreach (PropertyInfo propertyInfo in properties)
+ {
+ string propertyName = propertyInfo.Name;
+
+ // Ignore indexers and virtual properties that have overrides that were [JsonIgnore]d.
+ if (propertyInfo.GetIndexParameters().Length > 0 ||
+ PropertyIsOverriddenAndIgnored(propertyName, propertyInfo.PropertyType, propertyInfo.IsVirtual(), ignoredMembers))
{
- string propertyName = propertyInfo.Name;
+ continue;
+ }
- // Ignore indexers and virtual properties that have overrides that were [JsonIgnore]d.
- if (propertyInfo.GetIndexParameters().Length > 0 ||
- PropertyIsOverriddenAndIgnored(propertyName, propertyInfo.PropertyType, propertyInfo.IsVirtual(), ignoredMembers))
+ // For now we only support public properties (i.e. setter and/or getter is public).
+ if (propertyInfo.GetMethod?.IsPublic == true ||
+ propertyInfo.SetMethod?.IsPublic == true)
+ {
+ CacheMember(
+ typeToConvert: propertyInfo.PropertyType,
+ memberInfo: propertyInfo,
+ ref propertyOrderSpecified,
+ ref ignoredMembers,
+ shouldCheckMembersForRequiredMemberAttribute);
+ }
+ else
+ {
+ if (propertyInfo.GetCustomAttribute(inherit: false) != null)
{
- continue;
+ ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(propertyName, currentType);
}
- // For now we only support public properties (i.e. setter and/or getter is public).
- if (propertyInfo.GetMethod?.IsPublic == true ||
- propertyInfo.SetMethod?.IsPublic == true)
+ // Non-public properties should not be included for (de)serialization.
+ }
+ }
+
+ foreach (FieldInfo fieldInfo in currentType.GetFields(BindingFlags))
+ {
+ string fieldName = fieldInfo.Name;
+
+ if (PropertyIsOverriddenAndIgnored(fieldName, fieldInfo.FieldType, currentMemberIsVirtual: false, ignoredMembers))
+ {
+ continue;
+ }
+
+ bool hasJsonInclude = fieldInfo.GetCustomAttribute(inherit: false) != null;
+
+ if (fieldInfo.IsPublic)
+ {
+ if (hasJsonInclude || Options.IncludeFields)
{
CacheMember(
- typeToConvert: propertyInfo.PropertyType,
- memberInfo: propertyInfo,
+ typeToConvert: fieldInfo.FieldType,
+ memberInfo: fieldInfo,
ref propertyOrderSpecified,
ref ignoredMembers,
shouldCheckMembersForRequiredMemberAttribute);
}
- else
- {
- if (propertyInfo.GetCustomAttribute(inherit: false) != null)
- {
- ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(propertyName, currentType);
- }
-
- // Non-public properties should not be included for (de)serialization.
- }
}
-
- foreach (FieldInfo fieldInfo in currentType.GetFields(BindingFlags))
+ else
{
- string fieldName = fieldInfo.Name;
-
- if (PropertyIsOverriddenAndIgnored(fieldName, fieldInfo.FieldType, currentMemberIsVirtual: false, ignoredMembers))
+ if (hasJsonInclude)
{
- continue;
+ ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(fieldName, currentType);
}
- bool hasJsonInclude = fieldInfo.GetCustomAttribute(inherit: false) != null;
-
- if (fieldInfo.IsPublic)
- {
- if (hasJsonInclude || Options.IncludeFields)
- {
- CacheMember(
- typeToConvert: fieldInfo.FieldType,
- memberInfo: fieldInfo,
- ref propertyOrderSpecified,
- ref ignoredMembers,
- shouldCheckMembersForRequiredMemberAttribute);
- }
- }
- else
- {
- if (hasJsonInclude)
- {
- ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(fieldName, currentType);
- }
-
- // Non-public fields should not be included for (de)serialization.
- }
+ // Non-public fields should not be included for (de)serialization.
}
}
-
- Debug.Assert(PropertyCache != null);
-
- if (propertyOrderSpecified)
- {
- PropertyCache.List.StableSortByKey(static p => p.Value.Order);
- }
}
private void CacheMember(
diff --git a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs
index bf8be277756723..e0e4e3ab2dd067 100644
--- a/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs
+++ b/src/libraries/System.Text.Json/tests/Common/PropertyVisibilityTests.cs
@@ -2850,5 +2850,161 @@ public class ClassWithCallbacks
public Action Action { get; set; } = (val) => Console.WriteLine();
}
+
+ [Fact]
+ public async Task SimpleInterfaceHierarchy_Serialization_ShouldIncludeBaseInterfaceProperties()
+ {
+ var value = new ISimpleInterfaceHierarchy.Implementation
+ {
+ BaseProperty = 0,
+ DerivedProperty = 1,
+ DerivedProperty2 = 2,
+ };
+
+ string json = await Serializer.SerializeWrapper(value);
+ Assert.Equal("""{"DerivedProperty":1,"BaseProperty":0}""", json);
+
+ json = await Serializer.SerializeWrapper(value);
+ Assert.Equal("""{"DerivedProperty2":2,"DerivedProperty":1,"BaseProperty":0}""", json);
+ }
+
+ public interface ISimpleInterfaceHierarchy
+ {
+ public int BaseProperty { get; set; }
+
+ public interface IDerivedInterface1 : ISimpleInterfaceHierarchy
+ {
+ public int DerivedProperty { get; set; }
+ }
+
+ public interface IDerivedInterface2 : IDerivedInterface1
+ {
+ public int DerivedProperty2 { get; set; }
+ }
+
+ public class Implementation : IDerivedInterface2
+ {
+ public int BaseProperty { get; set; }
+ public int DerivedProperty { get; set; }
+ public int DerivedProperty2 { get; set; }
+ }
+ }
+
+ [Fact]
+ public async Task DiamondInterfaceHierarchy_Serialization_ShouldIncludeBaseInterfaceProperties()
+ {
+ var value = new IDiamondInterfaceHierarchy.Implementation
+ {
+ BaseProperty = 0,
+ DerivedProperty = 1,
+ DerivedProperty2 = 2,
+ DerivedProperty3 = 3,
+ };
+
+ string json = await Serializer.SerializeWrapper(value);
+ Assert.Equal("""{"DerivedProperty":1,"BaseProperty":0}""", json);
+
+ json = await Serializer.SerializeWrapper(value);
+ Assert.Equal("""{"DerivedProperty2":2,"BaseProperty":0}""", json);
+
+ json = await Serializer.SerializeWrapper(value);
+ JsonTestHelper.AssertJsonEqual("""{"DerivedProperty3":3,"DerivedProperty2":2,"DerivedProperty":1,"BaseProperty":0}""", json);
+ }
+
+ public interface IDiamondInterfaceHierarchy
+ {
+ public int BaseProperty { get; set; }
+
+ public interface IDerivedInterface1 : IDiamondInterfaceHierarchy
+ {
+ public int DerivedProperty { get; set; }
+ }
+
+ public interface IDerivedInterface2 : IDiamondInterfaceHierarchy
+ {
+ public int DerivedProperty2 { get; set; }
+ }
+
+ public interface IJoinInterface : IDerivedInterface1, IDerivedInterface2
+ {
+ public int DerivedProperty3 { get; set; }
+ }
+
+ public class Implementation : IJoinInterface
+ {
+ public int BaseProperty { get; set; }
+ public int DerivedProperty { get; set; }
+ public int DerivedProperty2 { get; set; }
+ public int DerivedProperty3 { get; set; }
+ }
+ }
+
+ [Fact]
+ public async Task DiamondInterfaceHierarchyWithNamingConflict_ThrowsJsonException()
+ {
+ var value = new IDiamondInterfaceHierarchyWithNamingConflict.Implementation
+ {
+ DerivedProperty = 1,
+ };
+
+ await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value));
+ }
+
+ public interface IDiamondInterfaceHierarchyWithNamingConflict
+ {
+ public interface IDerivedInterface1 : IDiamondInterfaceHierarchyWithNamingConflict
+ {
+ public int DerivedProperty { get; set; }
+ }
+
+ public interface IDerivedInterface2 : IDiamondInterfaceHierarchyWithNamingConflict
+ {
+ public int DerivedProperty { get; set; }
+ }
+
+ public interface IJoinInterface : IDerivedInterface1, IDerivedInterface2
+ {
+ }
+
+ public class Implementation : IJoinInterface
+ {
+ public int DerivedProperty { get; set; }
+ }
+ }
+
+ [Fact]
+ public async Task DiamondInterfaceHierarchyWithNamingConflict_UsingJsonPropertyName_WorksAsExpected()
+ {
+ var value = new IDiamondInterfaceHierarchyWithNamingConflictUsingAttribute.Implementation
+ {
+ DerivedProperty = 1,
+ };
+
+ string json = await Serializer.SerializeWrapper(value);
+ JsonTestHelper.AssertJsonEqual("""{"DerivedProperty":1,"DerivedProperty2":1}""", json);
+ }
+
+ public interface IDiamondInterfaceHierarchyWithNamingConflictUsingAttribute
+ {
+ public interface IDerivedInterface1 : IDiamondInterfaceHierarchyWithNamingConflictUsingAttribute
+ {
+ public int DerivedProperty { get; set; }
+ }
+
+ public interface IDerivedInterface2 : IDiamondInterfaceHierarchyWithNamingConflictUsingAttribute
+ {
+ [JsonPropertyName("DerivedProperty2")]
+ public int DerivedProperty { get; set; }
+ }
+
+ public interface IJoinInterface : IDerivedInterface1, IDerivedInterface2
+ {
+ }
+
+ public class Implementation : IJoinInterface
+ {
+ public int DerivedProperty { get; set; }
+ }
+ }
}
}
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs
index 624854c1bca3ff..f43228d76b796b 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs
@@ -279,6 +279,13 @@ public override async Task HonorJsonPropertyName_PrivateSetter()
[JsonSerializable(typeof(TypeWith_IgnoredPropWith_BadConverter))]
[JsonSerializable(typeof(ClassWithIgnoredCallbacks))]
[JsonSerializable(typeof(ClassWithCallbacks))]
+ [JsonSerializable(typeof(ISimpleInterfaceHierarchy.IDerivedInterface1))]
+ [JsonSerializable(typeof(ISimpleInterfaceHierarchy.IDerivedInterface2))]
+ [JsonSerializable(typeof(IDiamondInterfaceHierarchy.IDerivedInterface1), TypeInfoPropertyName = "IDiamondInterfaceHierarchyIDerivedInterface1")]
+ [JsonSerializable(typeof(IDiamondInterfaceHierarchy.IDerivedInterface2), TypeInfoPropertyName = "IDiamondInterfaceHierarchyIDerivedInterface2")]
+ [JsonSerializable(typeof(IDiamondInterfaceHierarchy.IJoinInterface))]
+ [JsonSerializable(typeof(IDiamondInterfaceHierarchyWithNamingConflict.IJoinInterface), TypeInfoPropertyName = "IDiamondInterfaceHierarchyWithNamingConflictIJoinInterface")]
+ [JsonSerializable(typeof(IDiamondInterfaceHierarchyWithNamingConflictUsingAttribute.IJoinInterface), TypeInfoPropertyName = "IDiamondInterfaceHierarchyWithNamingConflictUsingAttributeIJoinInterface")]
internal sealed partial class PropertyVisibilityTestsContext_Metadata : JsonSerializerContext
{
}
@@ -514,6 +521,13 @@ public static void PublicContexAndJsonSerializerOptions()
[JsonSerializable(typeof(TypeWith_IgnoredPropWith_BadConverter))]
[JsonSerializable(typeof(ClassWithIgnoredCallbacks))]
[JsonSerializable(typeof(ClassWithCallbacks))]
+ [JsonSerializable(typeof(ISimpleInterfaceHierarchy.IDerivedInterface1))]
+ [JsonSerializable(typeof(ISimpleInterfaceHierarchy.IDerivedInterface2))]
+ [JsonSerializable(typeof(IDiamondInterfaceHierarchy.IDerivedInterface1), TypeInfoPropertyName = "IDiamondInterfaceHierarchyIDerivedInterface1")]
+ [JsonSerializable(typeof(IDiamondInterfaceHierarchy.IDerivedInterface2), TypeInfoPropertyName = "IDiamondInterfaceHierarchyIDerivedInterface2")]
+ [JsonSerializable(typeof(IDiamondInterfaceHierarchy.IJoinInterface))]
+ [JsonSerializable(typeof(IDiamondInterfaceHierarchyWithNamingConflict.IJoinInterface), TypeInfoPropertyName = "IDiamondInterfaceHierarchyWithNamingConflictIJoinInterface")]
+ [JsonSerializable(typeof(IDiamondInterfaceHierarchyWithNamingConflictUsingAttribute.IJoinInterface), TypeInfoPropertyName = "IDiamondInterfaceHierarchyWithNamingConflictUsingAttributeIJoinInterface")]
internal sealed partial class PropertyVisibilityTestsContext_Default : JsonSerializerContext
{
}