diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs
index 3f929d87378485..75552ccf382fbd 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs
@@ -113,6 +113,33 @@ private static void PopulateProperties(JsonTypeInfo typeInfo)
}
}
+ private const BindingFlags AllInstanceMembers =
+ BindingFlags.Instance |
+ BindingFlags.Public |
+ BindingFlags.NonPublic |
+ BindingFlags.DeclaredOnly;
+
+ ///
+ /// Looks up the type for a member matching the given name and member type.
+ ///
+ [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
+ internal static MemberInfo? LookupMemberInfo(Type type, MemberTypes memberType, string name)
+ {
+ Debug.Assert(memberType is MemberTypes.Field or MemberTypes.Property);
+
+ // Walk the type hierarchy starting from the current type up to the base type(s)
+ foreach (Type t in type.GetSortedTypeHierarchy())
+ {
+ MemberInfo[] members = t.GetMember(name, memberType, AllInstanceMembers);
+ if (members.Length > 0)
+ {
+ return members[0];
+ }
+ }
+
+ return null;
+ }
+
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
private static void AddMembersDeclaredBySuperType(
@@ -124,17 +151,11 @@ private static void AddMembersDeclaredBySuperType(
Debug.Assert(!typeInfo.IsReadOnly);
Debug.Assert(currentType.IsAssignableFrom(typeInfo.Type));
- const BindingFlags BindingFlags =
- BindingFlags.Instance |
- BindingFlags.Public |
- BindingFlags.NonPublic |
- BindingFlags.DeclaredOnly;
-
// Compiler adds RequiredMemberAttribute to type if any of the members are marked with 'required' keyword.
bool shouldCheckMembersForRequiredMemberAttribute =
!constructorHasSetsRequiredMembersAttribute && currentType.HasRequiredMemberAttribute();
- foreach (PropertyInfo propertyInfo in currentType.GetProperties(BindingFlags))
+ foreach (PropertyInfo propertyInfo in currentType.GetProperties(AllInstanceMembers))
{
// Ignore indexers and virtual properties that have overrides that were [JsonIgnore]d.
if (propertyInfo.GetIndexParameters().Length > 0 ||
@@ -160,7 +181,7 @@ private static void AddMembersDeclaredBySuperType(
}
}
- foreach (FieldInfo fieldInfo in currentType.GetFields(BindingFlags))
+ foreach (FieldInfo fieldInfo in currentType.GetFields(AllInstanceMembers))
{
bool hasJsonIncludeAtribute = fieldInfo.GetCustomAttribute(inherit: false) != null;
if (hasJsonIncludeAtribute || (fieldInfo.IsPublic && typeInfo.Options.IncludeFields))
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs
index 965b4cea39570a..5c1de5c199b1fe 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Helpers.cs
@@ -190,6 +190,7 @@ private static JsonPropertyInfo CreatePropertyInfoCore(JsonPropertyInfoVal
propertyInfo.IgnoreCondition = propertyInfoValues.IgnoreCondition;
propertyInfo.JsonTypeInfo = propertyInfoValues.PropertyTypeInfo;
propertyInfo.NumberHandling = propertyInfoValues.NumberHandling;
+ propertyInfo.IsSourceGenerated = true;
return propertyInfo;
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs
index e2234093474e0d..959490b53f19fc 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs
@@ -5,6 +5,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
+using System.Threading;
namespace System.Text.Json.Serialization.Metadata
{
@@ -162,17 +163,39 @@ internal JsonIgnoreCondition? IgnoreCondition
///
public ICustomAttributeProvider? AttributeProvider
{
- get => _attributeProvider;
+ get
+ {
+ ICustomAttributeProvider attributeProvider = _attributeProvider ?? InitializeAttributeProvider();
+ return ReferenceEquals(attributeProvider, s_nullAttributeProvider) ? null : attributeProvider;
+ }
set
{
VerifyMutable();
- _attributeProvider = value;
+ _attributeProvider = value ?? s_nullAttributeProvider;
}
}
- private JsonObjectCreationHandling? _objectCreationHandling;
- internal JsonObjectCreationHandling EffectiveObjectCreationHandling { get; private set; }
+ // Because the getter can initialize its own backing field, we want to avoid races between the getter and setter.
+ // This is done using CAS on the single _attributeProvider field which employs the following encoding:
+ // null: not initialized, s_nullAttributeProvider: null, otherwise: _attributeProvider
+ private ICustomAttributeProvider? _attributeProvider;
+ private static readonly ICustomAttributeProvider s_nullAttributeProvider = typeof(NullAttributeProviderPlaceholder);
+ private sealed class NullAttributeProviderPlaceholder;
+
+ [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode",
+ Justification = "Looks up members that are already being referenced by the source generator.")]
+ private ICustomAttributeProvider InitializeAttributeProvider()
+ {
+ // If the property is source generated, perform a reflection lookup of its MemberInfo.
+ // Avoids overhead of reflection at startup and makes this method trimmable if unused.
+ ICustomAttributeProvider? provider = IsSourceGenerated && MemberName != null
+ ? DefaultJsonTypeInfoResolver.LookupMemberInfo(DeclaringType, MemberType, MemberName)
+ : null;
+
+ provider ??= s_nullAttributeProvider;
+ return Interlocked.CompareExchange(ref _attributeProvider, provider, null) ?? provider;
+ }
///
/// Gets or sets a value indicating if the property or field should be replaced or populated during deserialization.
@@ -202,10 +225,13 @@ public JsonObjectCreationHandling? ObjectCreationHandling
}
}
- private ICustomAttributeProvider? _attributeProvider;
+ private JsonObjectCreationHandling? _objectCreationHandling;
+ internal JsonObjectCreationHandling EffectiveObjectCreationHandling { get; private set; }
+
internal string? MemberName { get; set; }
internal MemberTypes MemberType { get; set; }
internal bool IsVirtual { get; set; }
+ internal bool IsSourceGenerated { get; set; }
///
/// Specifies whether the current property is a special extension data property.
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs
index ed29ed8fd4e149..182981002b742b 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/ContextClasses.cs
@@ -58,6 +58,7 @@ public interface ITestContext
public JsonTypeInfo TypeWithDerivedAttribute { get; }
public JsonTypeInfo PolymorphicClass { get; }
public JsonTypeInfo PocoWithNumberHandlingAttr { get; }
+ public JsonTypeInfo PocoWithMixedVisibilityMembers { get; }
}
internal partial class JsonContext : JsonSerializerContext
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs
index 4ff998bd5d6ddc..308a6b8a104600 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataAndSerializationContextTests.cs
@@ -54,6 +54,7 @@ namespace System.Text.Json.SourceGeneration.Tests
[JsonSerializable(typeof(TypeWithDerivedAttribute))]
[JsonSerializable(typeof(PolymorphicClass))]
[JsonSerializable(typeof(PocoWithNumberHandlingAttr))]
+ [JsonSerializable(typeof(PocoWithMixedVisibilityMembers))]
internal partial class MetadataAndSerializationContext : JsonSerializerContext, ITestContext
{
public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Default;
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs
index 33683dfa09c685..10e394518cdf5e 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MetadataContextTests.cs
@@ -53,6 +53,7 @@ namespace System.Text.Json.SourceGeneration.Tests
[JsonSerializable(typeof(TypeWithDerivedAttribute), GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(PolymorphicClass), GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(PocoWithNumberHandlingAttr), GenerationMode = JsonSourceGenerationMode.Metadata)]
+ [JsonSerializable(typeof(PocoWithMixedVisibilityMembers), GenerationMode = JsonSourceGenerationMode.Metadata)]
internal partial class MetadataWithPerTypeAttributeContext : JsonSerializerContext, ITestContext
{
public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Metadata;
@@ -156,6 +157,7 @@ public override void EnsureFastPathGeneratedAsExpected()
[JsonSerializable(typeof(TypeWithDerivedAttribute))]
[JsonSerializable(typeof(PolymorphicClass))]
[JsonSerializable(typeof(PocoWithNumberHandlingAttr))]
+ [JsonSerializable(typeof(PocoWithMixedVisibilityMembers))]
internal partial class MetadataContext : JsonSerializerContext, ITestContext
{
public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Metadata;
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs
index 72b9a9baff0940..529bd598b6ea25 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs
@@ -54,6 +54,7 @@ namespace System.Text.Json.SourceGeneration.Tests
[JsonSerializable(typeof(TypeWithDerivedAttribute), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(PolymorphicClass), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(PocoWithNumberHandlingAttr), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
+ [JsonSerializable(typeof(PocoWithMixedVisibilityMembers), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)]
internal partial class MixedModeContext : JsonSerializerContext, ITestContext
{
public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization;
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs
index 42f75fcb019d7f..00ce5d49d45131 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Xunit;
@@ -1111,5 +1112,36 @@ public void NumberHandlingHonoredOnPoco()
JsonTestHelper.AssertJsonEqual(@"{""Id"":""0""}", JsonSerializer.Serialize(new PocoWithNumberHandlingAttr(), DefaultContext.PocoWithNumberHandlingAttr));
}
}
+
+ [Theory]
+ [InlineData(MemberTypes.Property, nameof(PocoWithMixedVisibilityMembers.PublicProperty))]
+ [InlineData(MemberTypes.Field, nameof(PocoWithMixedVisibilityMembers.PublicField))]
+ [InlineData(MemberTypes.Property, nameof(PocoWithMixedVisibilityMembers.InternalProperty))]
+ [InlineData(MemberTypes.Field, nameof(PocoWithMixedVisibilityMembers.InternalField))]
+ [InlineData(MemberTypes.Property, nameof(PocoWithMixedVisibilityMembers.PropertyWithCustomName), "customProp")]
+ [InlineData(MemberTypes.Field, nameof(PocoWithMixedVisibilityMembers.FieldWithCustomName), "customField")]
+ [InlineData(MemberTypes.Property, nameof(PocoWithMixedVisibilityMembers.BaseProperty))]
+ [InlineData(MemberTypes.Property, nameof(PocoWithMixedVisibilityMembers.ShadowProperty))]
+ public void JsonPropertyInfo_PopulatesAttributeProvider(MemberTypes memberType, string propertyName, string? jsonPropertyName = null)
+ {
+ if (DefaultContext.JsonSourceGenerationMode is JsonSourceGenerationMode.Serialization)
+ {
+ return; // No metadata generated
+ }
+
+ JsonTypeInfo typeInfo = DefaultContext.PocoWithMixedVisibilityMembers;
+ string name = jsonPropertyName ?? propertyName;
+ JsonPropertyInfo prop = typeInfo.Properties.FirstOrDefault(prop => prop.Name == name);
+ Assert.NotNull(prop);
+
+ MemberInfo memberInfo = Assert.IsAssignableFrom(prop.AttributeProvider);
+ string? actualJsonPropertyName = memberInfo.GetCustomAttribute()?.Name;
+
+ Assert.True(memberInfo.DeclaringType.IsAssignableFrom(typeInfo.Type));
+ Assert.Equal(memberType, memberInfo.MemberType);
+ Assert.Equal(prop.PropertyType, memberInfo is PropertyInfo p ? p.PropertyType : ((FieldInfo)memberInfo).FieldType);
+ Assert.Equal(propertyName, memberInfo.Name);
+ Assert.Equal(jsonPropertyName, actualJsonPropertyName);
+ }
}
}
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs
index 5a7776640b4e8b..162a8b12bd7e74 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs
@@ -55,6 +55,7 @@ namespace System.Text.Json.SourceGeneration.Tests
[JsonSerializable(typeof(TypeWithDerivedAttribute))]
[JsonSerializable(typeof(PolymorphicClass))]
[JsonSerializable(typeof(PocoWithNumberHandlingAttr))]
+ [JsonSerializable(typeof(PocoWithMixedVisibilityMembers))]
internal partial class SerializationContext : JsonSerializerContext, ITestContext
{
public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Serialization;
@@ -109,6 +110,7 @@ internal partial class SerializationContext : JsonSerializerContext, ITestContex
[JsonSerializable(typeof(TypeWithDerivedAttribute), GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(PolymorphicClass), GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(PocoWithNumberHandlingAttr), GenerationMode = JsonSourceGenerationMode.Serialization)]
+ [JsonSerializable(typeof(PocoWithMixedVisibilityMembers), GenerationMode = JsonSourceGenerationMode.Serialization)]
internal partial class SerializationWithPerTypeAttributeContext : JsonSerializerContext, ITestContext
{
public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Serialization;
@@ -162,6 +164,7 @@ internal partial class SerializationWithPerTypeAttributeContext : JsonSerializer
[JsonSerializable(typeof(TypeWithDerivedAttribute), GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(PolymorphicClass), GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(PocoWithNumberHandlingAttr), GenerationMode = JsonSourceGenerationMode.Serialization)]
+ [JsonSerializable(typeof(PocoWithMixedVisibilityMembers), GenerationMode = JsonSourceGenerationMode.Serialization)]
internal partial class SerializationContextWithCamelCase : JsonSerializerContext, ITestContext
{
public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Serialization;
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs
index be241a0271d329..64a4f13818f068 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/TestClasses.cs
@@ -305,4 +305,32 @@ public class PocoWithNumberHandlingAttr
{
public int Id { get; set; }
}
+
+ public class PocoWithMixedVisibilityMembersBase
+ {
+ public string BaseProperty { get; set; }
+ public string ShadowProperty { get; set; }
+ }
+
+ public class PocoWithMixedVisibilityMembers : PocoWithMixedVisibilityMembersBase
+ {
+ public string PublicProperty { get; set; }
+
+ [JsonInclude]
+ public string PublicField;
+
+ [JsonInclude]
+ internal int InternalProperty { get; set; }
+
+ [JsonInclude]
+ internal int InternalField;
+
+ [JsonPropertyName("customProp")]
+ public string PropertyWithCustomName { get; set; }
+
+ [JsonInclude, JsonPropertyName("customField")]
+ public string FieldWithCustomName;
+
+ public new int ShadowProperty { get; set; }
+ }
}