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; } + } }