Skip to content

Commit 0f11212

Browse files
Implement the JsonSerializer.IsReflectionEnabledByDefault feature switch (#83844)
* Implement the STJ.DisableDefaultReflection feature switch. * Reinstate accidentally stripped attribute * Address feedback. * Address feedback. * Add a trimming test for STJ * Move trimming test to existing trimming tests folder. * Add source gen serialization test case to Trimming test. * Fix style. * Expose the feature switch as a property on JsonSerializer -- rename feature switch to match namespace. * Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs Co-authored-by: Eric Erhardt <eric.erhardt@microsoft.com> * Update src/libraries/System.Text.Json/tests/System.Text.Json.Tests/TrimmingTests/IsReflectionEnabledByDefaultFalse.cs Co-authored-by: Eric Erhardt <eric.erhardt@microsoft.com> * Address feedback. * Address feedback. * Add entry to feature-switches.md --------- Co-authored-by: Eric Erhardt <eric.erhardt@microsoft.com>
1 parent 042a350 commit 0f11212

17 files changed

+413
-79
lines changed

docs/workflow/trimming/feature-switches.md

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ configurations but their defaults might vary as any SDK can set the defaults dif
2929
| NullabilityInfoContextSupport | System.Reflection.NullabilityInfoContext.IsSupported | Nullable attributes can be trimmed when set to false |
3030
| DynamicCodeSupport | System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported | Changes RuntimeFeature.IsDynamicCodeSupported to false to allow testing AOT-safe fallback code without publishing for Native AOT. |
3131
| _AggressiveAttributeTrimming | System.AggressiveAttributeTrimming | When set to true, aggressively trims attributes to allow for the most size savings possible, even if it could result in runtime behavior changes |
32+
| JsonSerializerIsReflectionEnabledByDefault | System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault | When set to false, disables using reflection as the default contract resolver in System.Text.Json |
3233

3334
Any feature-switch which defines property can be set in csproj file or
3435
on the command line as any other MSBuild property. Those without predefined property name

src/libraries/System.Text.Json/ref/System.Text.Json.cs

+1
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ public static partial class JsonSerializer
280280
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")]
281281
public static TValue? Deserialize<TValue>(ref System.Text.Json.Utf8JsonReader reader, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
282282
public static TValue? Deserialize<TValue>(ref System.Text.Json.Utf8JsonReader reader, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TValue> jsonTypeInfo) { throw null; }
283+
public static bool IsReflectionEnabledByDefault { get { throw null; } }
283284
public static void Serialize(System.IO.Stream utf8Json, object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo jsonTypeInfo) { }
284285
[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")]
285286
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<linker>
2+
<assembly fullname="System.Text.Json">
3+
<type fullname="System.Text.Json.JsonSerializer">
4+
<method signature="System.Boolean get_IsReflectionEnabledByDefault()" body="stub" value="false"
5+
feature="System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault" featurevalue="false"/>
6+
</type>
7+
</assembly>
8+
</linker>

src/libraries/System.Text.Json/src/System.Text.Json.csproj

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
1818
<NoWarn Condition="$([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)')) != '.NETCoreApp'">$(NoWarn);nullable</NoWarn>
1919
</PropertyGroup>
2020

21+
<ItemGroup>
22+
<ILLinkSubstitutionsXmls Include="ILLink\ILLink.Substitutions.xml" />
23+
</ItemGroup>
24+
2125
<ItemGroup>
2226
<Compile Include="$(CommonPath)System\HexConverter.cs" Link="Common\System\HexConverter.cs" />
2327
<Compile Include="$(CommonPath)System\Text\Json\PooledByteBufferWriter.cs" Link="Common\System\Text\Json\PooledByteBufferWriter.cs" />

src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs

+1-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ namespace System.Text.Json
55
{
66
internal static class AppContextSwitchHelper
77
{
8-
public static bool IsSourceGenReflectionFallbackEnabled => s_isSourceGenReflectionFallbackEnabled;
9-
10-
private static readonly bool s_isSourceGenReflectionFallbackEnabled =
8+
public static bool IsSourceGenReflectionFallbackEnabled { get; } =
119
AppContext.TryGetSwitch(
1210
switchName: "System.Text.Json.Serialization.EnableSourceGenReflectionFallback",
1311
isEnabled: out bool value)

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs

+16-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ public static partial class JsonSerializer
1313
internal const string SerializationUnreferencedCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.";
1414
internal const string SerializationRequiresDynamicCodeMessage = "JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.";
1515

16+
/// <summary>
17+
/// Indicates whether unconfigured <see cref="JsonSerializerOptions"/> instances
18+
/// should be set to use the reflection-based <see cref="DefaultJsonTypeInfoResolver"/>.
19+
/// </summary>
20+
/// <remarks>
21+
/// The value of the property is backed by the "System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault"
22+
/// <see cref="AppContext"/> setting and defaults to <see langword="true"/> if unset.
23+
/// </remarks>
24+
public static bool IsReflectionEnabledByDefault { get; } =
25+
AppContext.TryGetSwitch(
26+
switchName: "System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault",
27+
isEnabled: out bool value)
28+
? value : true;
29+
1630
[RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]
1731
[RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)]
1832
private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type inputType, bool fallBackToNearestAncestorType = false)
@@ -21,9 +35,9 @@ private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type inp
2135

2236
options ??= JsonSerializerOptions.Default;
2337

24-
if (!options.IsInitializedForReflectionSerializer)
38+
if (!options.IsConfiguredForJsonSerializer)
2539
{
26-
options.InitializeForReflectionSerializer();
40+
options.ConfigureForJsonSerializer();
2741
}
2842

2943
// In order to improve performance of polymorphic root-level object serialization,

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace System.Text.Json.Serialization
99
/// <summary>
1010
/// Provides metadata about a set of types that is relevant to JSON serialization.
1111
/// </summary>
12-
public abstract partial class JsonSerializerContext : IJsonTypeInfoResolver
12+
public abstract partial class JsonSerializerContext : IJsonTypeInfoResolver, IBuiltInJsonTypeInfoResolver
1313
{
1414
private JsonSerializerOptions? _options;
1515

@@ -49,7 +49,7 @@ internal void AssociateWithOptions(JsonSerializerOptions options)
4949
/// Indicates whether pre-generated serialization logic for types in the context
5050
/// is compatible with the run time specified <see cref="JsonSerializerOptions"/>.
5151
/// </summary>
52-
internal bool IsCompatibleWithGeneratedOptions(JsonSerializerOptions options)
52+
bool IBuiltInJsonTypeInfoResolver.IsCompatibleWithOptions(JsonSerializerOptions options)
5353
{
5454
Debug.Assert(options != null);
5555

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public JsonConverter GetConverter(Type typeToConvert)
4545
ThrowHelper.ThrowArgumentNullException(nameof(typeToConvert));
4646
}
4747

48-
if (_typeInfoResolver is null)
48+
if (JsonSerializer.IsReflectionEnabledByDefault && _typeInfoResolver is null)
4949
{
5050
// Backward compatibility -- root & query the default reflection converters
5151
// but do not populate the TypeInfoResolver setting.

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs

+44-59
Original file line numberDiff line numberDiff line change
@@ -130,19 +130,6 @@ public JsonSerializerOptions(JsonSerializerOptions options)
130130
TrackOptionsInstance(this);
131131
}
132132

133-
/// <summary>Tracks the options instance to enable all instances to be enumerated.</summary>
134-
private static void TrackOptionsInstance(JsonSerializerOptions options) => TrackedOptionsInstances.All.Add(options, null);
135-
136-
internal static class TrackedOptionsInstances
137-
{
138-
/// <summary>Tracks all live JsonSerializerOptions instances.</summary>
139-
/// <remarks>Instances are added to the table in their constructor.</remarks>
140-
public static ConditionalWeakTable<JsonSerializerOptions, object?> All { get; } =
141-
// TODO https://github.com/dotnet/runtime/issues/51159:
142-
// Look into linking this away / disabling it when hot reload isn't in use.
143-
new ConditionalWeakTable<JsonSerializerOptions, object?>();
144-
}
145-
146133
/// <summary>
147134
/// Constructs a new <see cref="JsonSerializerOptions"/> instance with a predefined set of options determined by the specified <see cref="JsonSerializerDefaults"/>.
148135
/// </summary>
@@ -161,6 +148,19 @@ public JsonSerializerOptions(JsonSerializerDefaults defaults) : this()
161148
}
162149
}
163150

151+
/// <summary>Tracks the options instance to enable all instances to be enumerated.</summary>
152+
private static void TrackOptionsInstance(JsonSerializerOptions options) => TrackedOptionsInstances.All.Add(options, null);
153+
154+
internal static class TrackedOptionsInstances
155+
{
156+
/// <summary>Tracks all live JsonSerializerOptions instances.</summary>
157+
/// <remarks>Instances are added to the table in their constructor.</remarks>
158+
public static ConditionalWeakTable<JsonSerializerOptions, object?> All { get; } =
159+
// TODO https://github.com/dotnet/runtime/issues/51159:
160+
// Look into linking this away / disabling it when hot reload isn't in use.
161+
new ConditionalWeakTable<JsonSerializerOptions, object?>();
162+
}
163+
164164
/// <summary>
165165
/// Binds current <see cref="JsonSerializerOptions"/> instance with a new instance of the specified <see cref="Serialization.JsonSerializerContext"/> type.
166166
/// </summary>
@@ -638,32 +638,7 @@ internal bool CanUseFastPathSerializationLogic
638638
{
639639
Debug.Assert(IsReadOnly);
640640
Debug.Assert(TypeInfoResolver != null);
641-
return _canUseFastPathSerializationLogic ??= CanUseFastPath(TypeInfoResolver);
642-
643-
bool CanUseFastPath(IJsonTypeInfoResolver resolver)
644-
{
645-
switch (resolver)
646-
{
647-
case DefaultJsonTypeInfoResolver defaultResolver:
648-
return defaultResolver.GetType() == typeof(DefaultJsonTypeInfoResolver) &&
649-
defaultResolver.Modifiers.Count == 0;
650-
case JsonSerializerContext ctx:
651-
return ctx.IsCompatibleWithGeneratedOptions(this);
652-
case JsonTypeInfoResolverChain resolverChain:
653-
foreach (IJsonTypeInfoResolver component in resolverChain)
654-
{
655-
if (!CanUseFastPath(component))
656-
{
657-
return false;
658-
}
659-
}
660-
661-
return true;
662-
663-
default:
664-
return false;
665-
}
666-
}
641+
return _canUseFastPathSerializationLogic ??= TypeInfoResolver.IsCompatibleWithOptions(this);
667642
}
668643
}
669644

@@ -699,35 +674,38 @@ public void MakeReadOnly()
699674
}
700675

701676
/// <summary>
702-
/// Initializes the converters for the reflection-based serializer.
677+
/// Configures the instance for use by the JsonSerializer APIs.
703678
/// </summary>
704679
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
705680
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
706-
internal void InitializeForReflectionSerializer()
681+
internal void ConfigureForJsonSerializer()
707682
{
708-
// Even if a resolver has already been specified, we need to root
709-
// the default resolver to gain access to the default converters.
710-
DefaultJsonTypeInfoResolver defaultResolver = DefaultJsonTypeInfoResolver.RootDefaultInstance();
711-
712-
switch (_typeInfoResolver)
683+
if (JsonSerializer.IsReflectionEnabledByDefault)
713684
{
714-
case null:
715-
// Use the default reflection-based resolver if no resolver has been specified.
716-
_typeInfoResolver = defaultResolver;
717-
break;
685+
// Even if a resolver has already been specified, we need to root
686+
// the default resolver to gain access to the default converters.
687+
DefaultJsonTypeInfoResolver defaultResolver = DefaultJsonTypeInfoResolver.RootDefaultInstance();
718688

719-
case JsonSerializerContext ctx when AppContextSwitchHelper.IsSourceGenReflectionFallbackEnabled:
720-
// .NET 6 compatibility mode: enable fallback to reflection metadata for JsonSerializerContext
721-
_effectiveJsonTypeInfoResolver = JsonTypeInfoResolver.Combine(ctx, defaultResolver);
722-
break;
689+
switch (_typeInfoResolver)
690+
{
691+
case null:
692+
// Use the default reflection-based resolver if no resolver has been specified.
693+
_typeInfoResolver = defaultResolver;
694+
break;
695+
696+
case JsonSerializerContext ctx when AppContextSwitchHelper.IsSourceGenReflectionFallbackEnabled:
697+
// .NET 6 compatibility mode: enable fallback to reflection metadata for JsonSerializerContext
698+
_effectiveJsonTypeInfoResolver = JsonTypeInfoResolver.Combine(ctx, defaultResolver);
699+
break;
700+
}
723701
}
724702

725703
MakeReadOnly();
726-
_isInitializedForReflectionSerializer = true;
704+
_isConfiguredForJsonSerializer = true;
727705
}
728706

729-
internal bool IsInitializedForReflectionSerializer => _isInitializedForReflectionSerializer;
730-
private volatile bool _isInitializedForReflectionSerializer;
707+
internal bool IsConfiguredForJsonSerializer => _isConfiguredForJsonSerializer;
708+
private volatile bool _isConfiguredForJsonSerializer;
731709

732710
// Only populated in .NET 6 compatibility mode encoding reflection fallback in source gen
733711
private IJsonTypeInfoResolver? _effectiveJsonTypeInfoResolver;
@@ -852,8 +830,15 @@ private static JsonSerializerOptions GetOrCreateDefaultOptionsInstance()
852830
{
853831
var options = new JsonSerializerOptions
854832
{
855-
TypeInfoResolver = DefaultJsonTypeInfoResolver.RootDefaultInstance(),
856-
_isReadOnly = true
833+
// Because we're marking the default instance as read-only,
834+
// we need to specify a resolver instance for the case where
835+
// reflection is disabled by default: use one that returns null for all types.
836+
837+
TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault
838+
? DefaultJsonTypeInfoResolver.RootDefaultInstance()
839+
: new JsonTypeInfoResolverChain(),
840+
841+
_isReadOnly = true,
857842
};
858843

859844
return Interlocked.CompareExchange(ref s_defaultOptions, options, null) ?? options;

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -399,25 +399,25 @@ internal static void DeterminePropertyAccessors<T>(JsonPropertyInfo<T> jsonPrope
399399
MethodInfo? getMethod = propertyInfo.GetMethod;
400400
if (getMethod != null && (getMethod.IsPublic || useNonPublicAccessors))
401401
{
402-
jsonPropertyInfo.Get = DefaultJsonTypeInfoResolver.MemberAccessor.CreatePropertyGetter<T>(propertyInfo);
402+
jsonPropertyInfo.Get = MemberAccessor.CreatePropertyGetter<T>(propertyInfo);
403403
}
404404

405405
MethodInfo? setMethod = propertyInfo.SetMethod;
406406
if (setMethod != null && (setMethod.IsPublic || useNonPublicAccessors))
407407
{
408-
jsonPropertyInfo.Set = DefaultJsonTypeInfoResolver.MemberAccessor.CreatePropertySetter<T>(propertyInfo);
408+
jsonPropertyInfo.Set = MemberAccessor.CreatePropertySetter<T>(propertyInfo);
409409
}
410410

411411
break;
412412

413413
case FieldInfo fieldInfo:
414414
Debug.Assert(fieldInfo.IsPublic);
415415

416-
jsonPropertyInfo.Get = DefaultJsonTypeInfoResolver.MemberAccessor.CreateFieldGetter<T>(fieldInfo);
416+
jsonPropertyInfo.Get = MemberAccessor.CreateFieldGetter<T>(fieldInfo);
417417

418418
if (!fieldInfo.IsInitOnly)
419419
{
420-
jsonPropertyInfo.Set = DefaultJsonTypeInfoResolver.MemberAccessor.CreateFieldSetter<T>(fieldInfo);
420+
jsonPropertyInfo.Set = MemberAccessor.CreateFieldSetter<T>(fieldInfo);
421421
}
422422

423423
break;

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace System.Text.Json.Serialization.Metadata
1313
/// <remarks>
1414
/// The contract resolver used by <see cref="JsonSerializerOptions.Default"/>.
1515
/// </remarks>
16-
public partial class DefaultJsonTypeInfoResolver : IJsonTypeInfoResolver
16+
public partial class DefaultJsonTypeInfoResolver : IJsonTypeInfoResolver, IBuiltInJsonTypeInfoResolver
1717
{
1818
private bool _mutable;
1919

@@ -122,6 +122,11 @@ protected override void OnCollectionModifying()
122122
}
123123
}
124124

125+
bool IBuiltInJsonTypeInfoResolver.IsCompatibleWithOptions(JsonSerializerOptions _)
126+
// Metadata generated by the default resolver is compatible by definition,
127+
// provided that no user extensions have been made on the class.
128+
=> _modifiers is null or { Count: 0 } && GetType() == typeof(DefaultJsonTypeInfoResolver);
129+
125130
internal static bool IsDefaultInstanceRooted => s_defaultInstance is not null;
126131
private static DefaultJsonTypeInfoResolver? s_defaultInstance;
127132

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

+1-6
Original file line numberDiff line numberDiff line change
@@ -695,12 +695,7 @@ bool IsCurrentNodeCompatible()
695695
return false;
696696
}
697697

698-
return OriginatingResolver switch
699-
{
700-
JsonSerializerContext ctx => ctx.IsCompatibleWithGeneratedOptions(Options),
701-
DefaultJsonTypeInfoResolver => true, // generates default contracts by definition
702-
_ => false
703-
};
698+
return OriginatingResolver.IsCompatibleWithOptions(Options);
704699
}
705700
}
706701

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

+20
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,25 @@ public static IJsonTypeInfoResolver Combine(params IJsonTypeInfoResolver?[] reso
3838

3939
return resolverChain.Count == 1 ? resolverChain[0] : resolverChain;
4040
}
41+
42+
/// <summary>
43+
/// Indicates whether the metadata generated by the current resolver
44+
/// are compatible with the run time specified <see cref="JsonSerializerOptions"/>.
45+
/// </summary>
46+
internal static bool IsCompatibleWithOptions(this IJsonTypeInfoResolver? resolver, JsonSerializerOptions options)
47+
=> resolver is IBuiltInJsonTypeInfoResolver bir && bir.IsCompatibleWithOptions(options);
48+
}
49+
50+
/// <summary>
51+
/// Implemented by the built-in converters to avoid rooting
52+
/// unused resolver dependencies in the context of the trimmer.
53+
/// </summary>
54+
internal interface IBuiltInJsonTypeInfoResolver
55+
{
56+
/// <summary>
57+
/// Indicates whether the metadata generated by the current resolver
58+
/// are compatible with the run time specified <see cref="JsonSerializerOptions"/>.
59+
/// </summary>
60+
bool IsCompatibleWithOptions(JsonSerializerOptions options);
4161
}
4262
}

0 commit comments

Comments
 (0)