Skip to content

Commit 227e882

Browse files
Fix support for non-public default constructors using JsonIncludeAttribute (#90612)
* Fix support for non-public constructors using JsonIncludeAttribute * Address feedback.
1 parent c7cb08c commit 227e882

File tree

8 files changed

+95
-30
lines changed

8 files changed

+95
-30
lines changed

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte
4949
typeInfo.PopulatePolymorphismMetadata();
5050
typeInfo.MapInterfaceTypesToCallbacks();
5151

52-
Func<object>? createObject = MemberAccessor.CreateConstructor(typeInfo.Type);
52+
Func<object>? createObject = DetermineCreateObjectDelegate(type, converter);
5353
typeInfo.SetCreateObjectIfCompatible(createObject);
5454
typeInfo.CreateObjectForExtensionDataProperty = createObject;
5555

@@ -411,5 +411,24 @@ internal static void DeterminePropertyAccessors<T>(JsonPropertyInfo<T> jsonPrope
411411
break;
412412
}
413413
}
414+
415+
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
416+
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
417+
private static Func<object>? DetermineCreateObjectDelegate(Type type, JsonConverter converter)
418+
{
419+
ConstructorInfo? defaultCtor = null;
420+
421+
if (converter.ConstructorInfo != null && !converter.ConstructorIsParameterized)
422+
{
423+
// A parameterless constructor has been resolved by the converter
424+
// (e.g. it might be a non-public ctor with JsonConverterAttribute).
425+
defaultCtor = converter.ConstructorInfo;
426+
}
427+
428+
// Fall back to resolving any public constructors on the type.
429+
defaultCtor ??= type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, binder: null, Type.EmptyTypes, modifiers: null);
430+
431+
return MemberAccessor.CreateParameterlessConstructor(type, defaultCtor);
432+
}
414433
}
415434
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/MemberAccessor.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ namespace System.Text.Json.Serialization.Metadata
99
{
1010
internal abstract class MemberAccessor
1111
{
12-
public abstract Func<object>? CreateConstructor(
13-
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type classType);
12+
public abstract Func<object>? CreateParameterlessConstructor(
13+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type,
14+
ConstructorInfo? constructorInfo);
1415

1516
public abstract Func<object[], T> CreateParameterizedConstructor<T>(ConstructorInfo constructor);
1617

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ internal sealed partial class ReflectionEmitCachingMemberAccessor : MemberAccess
2121
=> s_cache.GetOrAdd((nameof(CreateAddMethodDelegate), typeof(TCollection), null),
2222
static (_) => s_sourceAccessor.CreateAddMethodDelegate<TCollection>());
2323

24-
public override Func<object>? CreateConstructor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type classType)
25-
=> s_cache.GetOrAdd((nameof(CreateConstructor), classType, null),
24+
public override Func<object>? CreateParameterlessConstructor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type, ConstructorInfo? ctorInfo)
25+
=> s_cache.GetOrAdd((nameof(CreateParameterlessConstructor), type, ctorInfo),
2626
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2077:UnrecognizedReflectionPattern",
2727
Justification = "Cannot apply DynamicallyAccessedMembersAttribute to tuple properties.")]
2828
#pragma warning disable IL2077 // The suppression doesn't work for the trim analyzer: https://github.com/dotnet/roslyn/issues/59746
29-
static (key) => s_sourceAccessor.CreateConstructor(key.declaringType));
29+
static (key) => s_sourceAccessor.CreateParameterlessConstructor(key.declaringType, (ConstructorInfo?)key.member));
3030
#pragma warning restore IL2077
3131

3232
public override Func<object, TProperty> CreateFieldGetter<TProperty>(FieldInfo fieldInfo)

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitMemberAccessor.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,19 @@ namespace System.Text.Json.Serialization.Metadata
1313
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
1414
internal sealed class ReflectionEmitMemberAccessor : MemberAccessor
1515
{
16-
public override Func<object>? CreateConstructor(
17-
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type)
16+
public override Func<object>? CreateParameterlessConstructor(
17+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type,
18+
ConstructorInfo? constructorInfo)
1819
{
1920
Debug.Assert(type != null);
20-
ConstructorInfo? realMethod = type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, binder: null, Type.EmptyTypes, modifiers: null);
21+
Debug.Assert(constructorInfo is null || constructorInfo.GetParameters().Length == 0);
2122

2223
if (type.IsAbstract)
2324
{
2425
return null;
2526
}
2627

27-
if (realMethod == null && !type.IsValueType)
28+
if (constructorInfo is null && !type.IsValueType)
2829
{
2930
return null;
3031
}
@@ -38,8 +39,10 @@ internal sealed class ReflectionEmitMemberAccessor : MemberAccessor
3839

3940
ILGenerator generator = dynamicMethod.GetILGenerator();
4041

41-
if (realMethod == null)
42+
if (constructorInfo is null)
4243
{
44+
Debug.Assert(type.IsValueType);
45+
4346
LocalBuilder local = generator.DeclareLocal(type);
4447

4548
generator.Emit(OpCodes.Ldloca_S, local);
@@ -49,7 +52,7 @@ internal sealed class ReflectionEmitMemberAccessor : MemberAccessor
4952
}
5053
else
5154
{
52-
generator.Emit(OpCodes.Newobj, realMethod);
55+
generator.Emit(OpCodes.Newobj, constructorInfo);
5356
if (type.IsValueType)
5457
{
5558
// Since C# 10 it's now possible to have parameterless constructors in structs

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,26 @@ namespace System.Text.Json.Serialization.Metadata
1010
{
1111
internal sealed class ReflectionMemberAccessor : MemberAccessor
1212
{
13-
private sealed class ConstructorContext
14-
{
15-
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
16-
private readonly Type _type;
17-
18-
public ConstructorContext([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type)
19-
=> _type = type;
20-
21-
public object? CreateInstance()
22-
=> Activator.CreateInstance(_type, nonPublic: false);
23-
}
24-
25-
public override Func<object>? CreateConstructor(
26-
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type)
13+
public override Func<object>? CreateParameterlessConstructor(
14+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type,
15+
ConstructorInfo? ctorInfo)
2716
{
2817
Debug.Assert(type != null);
29-
ConstructorInfo? realMethod = type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, binder: null, Type.EmptyTypes, modifiers: null);
18+
Debug.Assert(ctorInfo is null || ctorInfo.GetParameters().Length == 0);
3019

3120
if (type.IsAbstract)
3221
{
3322
return null;
3423
}
3524

36-
if (realMethod == null && !type.IsValueType)
25+
if (ctorInfo is null)
3726
{
38-
return null;
27+
return type.IsValueType
28+
? () => Activator.CreateInstance(type, nonPublic: false)!
29+
: null;
3930
}
4031

41-
return new ConstructorContext(type).CreateInstance!;
32+
return () => ctorInfo.Invoke(null);
4233
}
4334

4435
public override Func<object[], T> CreateParameterizedConstructor<T>(ConstructorInfo constructor)

src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.AttributePresence.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ public async Task NonPublicCtors_WithJsonConstructorAttribute_WorksAsExpected(Ty
3939
}
4040
}
4141

42+
[Theory]
43+
[InlineData(typeof(PrivateParameterlessCtor_WithAttribute), false)]
44+
[InlineData(typeof(InternalParameterlessCtor_WithAttribute), true)]
45+
[InlineData(typeof(ProtectedParameterlessCtor_WithAttribute), false)]
46+
public async Task NonPublicParameterlessCtors_WithJsonConstructorAttribute_WorksAsExpected(Type type, bool isAccessibleBySourceGen)
47+
{
48+
if (!Serializer.IsSourceGeneratedSerializer || isAccessibleBySourceGen)
49+
{
50+
object? result = await Serializer.DeserializeWrapper("{}", type);
51+
Assert.IsType(type, result);
52+
}
53+
else
54+
{
55+
NotSupportedException ex = await Assert.ThrowsAsync<NotSupportedException>(() => Serializer.DeserializeWrapper("{}", type));
56+
Assert.Contains("JsonConstructorAttribute", ex.ToString());
57+
}
58+
}
59+
4260
[Fact]
4361
public async Task SinglePublicParameterizedCtor_SingleParameterlessCtor_NoAttribute_Supported_UseParameterlessCtor()
4462
{

src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,33 @@ public class ProtectedParameterizedCtor_WithAttribute
7474
protected ProtectedParameterizedCtor_WithAttribute(int x) => X = x;
7575
}
7676

77+
public class PrivateParameterlessCtor_WithAttribute
78+
{
79+
public int X { get; }
80+
81+
[JsonConstructor]
82+
private PrivateParameterlessCtor_WithAttribute()
83+
=> X = 42;
84+
}
85+
86+
public class ProtectedParameterlessCtor_WithAttribute
87+
{
88+
public int X { get; }
89+
90+
[JsonConstructor]
91+
protected ProtectedParameterlessCtor_WithAttribute()
92+
=> X = 42;
93+
}
94+
95+
public class InternalParameterlessCtor_WithAttribute
96+
{
97+
public int X { get; }
98+
99+
[JsonConstructor]
100+
internal InternalParameterlessCtor_WithAttribute()
101+
=> X = 42;
102+
}
103+
77104
public class PrivateParameterlessCtor_InternalParameterizedCtor_WithMultipleAttributes
78105
{
79106
[JsonConstructor]

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ protected ConstructorTests_Metadata(JsonSerializerWrapper stringWrapper)
4141
[JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute))]
4242
[JsonSerializable(typeof(InternalParameterizedCtor_WithAttribute))]
4343
[JsonSerializable(typeof(ProtectedParameterizedCtor_WithAttribute))]
44+
[JsonSerializable(typeof(PrivateParameterlessCtor_WithAttribute))]
45+
[JsonSerializable(typeof(InternalParameterlessCtor_WithAttribute))]
46+
[JsonSerializable(typeof(ProtectedParameterlessCtor_WithAttribute))]
4447
[JsonSerializable(typeof(SinglePublicParameterizedCtor))]
4548
[JsonSerializable(typeof(SingleParameterlessCtor_MultiplePublicParameterizedCtor))]
4649
[JsonSerializable(typeof(SingleParameterlessCtor_MultiplePublicParameterizedCtor_Struct))]
@@ -186,6 +189,9 @@ public ConstructorTests_Default(JsonSerializerWrapper jsonSerializer) : base(jso
186189
[JsonSerializable(typeof(PrivateParameterizedCtor_WithAttribute))]
187190
[JsonSerializable(typeof(InternalParameterizedCtor_WithAttribute))]
188191
[JsonSerializable(typeof(ProtectedParameterizedCtor_WithAttribute))]
192+
[JsonSerializable(typeof(PrivateParameterlessCtor_WithAttribute))]
193+
[JsonSerializable(typeof(InternalParameterlessCtor_WithAttribute))]
194+
[JsonSerializable(typeof(ProtectedParameterlessCtor_WithAttribute))]
189195
[JsonSerializable(typeof(SinglePublicParameterizedCtor))]
190196
[JsonSerializable(typeof(SingleParameterlessCtor_MultiplePublicParameterizedCtor))]
191197
[JsonSerializable(typeof(SingleParameterlessCtor_MultiplePublicParameterizedCtor_Struct))]

0 commit comments

Comments
 (0)