Implement the JsonSerializer.IsReflectionEnabledByDefault feature switch (#83844)
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Wed, 5 Apr 2023 20:21:06 +0000 (21:21 +0100)
committerGitHub <noreply@github.com>
Wed, 5 Apr 2023 20:21:06 +0000 (21:21 +0100)
* 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>
17 files changed:
docs/workflow/trimming/feature-switches.md
src/libraries/System.Text.Json/ref/System.Text.Json.cs
src/libraries/System.Text.Json/src/ILLink/ILLink.Substitutions.xml [new file with mode: 0644]
src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Helpers.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolver.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolverChain.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/TrimmingTests/IsReflectionEnabledByDefaultFalse.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/TrimmingTests/System.Text.Json.TrimmingTests.proj

index 7600b3d..0aa4429 100644 (file)
@@ -29,6 +29,7 @@ configurations but their defaults might vary as any SDK can set the defaults dif
 | NullabilityInfoContextSupport | System.Reflection.NullabilityInfoContext.IsSupported | Nullable attributes can be trimmed when set to false |
 | DynamicCodeSupport | System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported | Changes RuntimeFeature.IsDynamicCodeSupported to false to allow testing AOT-safe fallback code without publishing for Native AOT. |
 | _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 |
+| JsonSerializerIsReflectionEnabledByDefault | System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault | When set to false, disables using reflection as the default contract resolver in System.Text.Json |
 
 Any feature-switch which defines property can be set in csproj file or
 on the command line as any other MSBuild property. Those without predefined property name
index 1242617..3002b59 100644 (file)
@@ -280,6 +280,7 @@ namespace System.Text.Json
         [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.")]
         public static TValue? Deserialize<TValue>(ref System.Text.Json.Utf8JsonReader reader, System.Text.Json.JsonSerializerOptions? options = null) { throw null; }
         public static TValue? Deserialize<TValue>(ref System.Text.Json.Utf8JsonReader reader, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TValue> jsonTypeInfo) { throw null; }
+        public static bool IsReflectionEnabledByDefault { get { throw null; } }
         public static void Serialize(System.IO.Stream utf8Json, object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo jsonTypeInfo) { }
         [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.")]
         [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.")]
diff --git a/src/libraries/System.Text.Json/src/ILLink/ILLink.Substitutions.xml b/src/libraries/System.Text.Json/src/ILLink/ILLink.Substitutions.xml
new file mode 100644 (file)
index 0000000..026bbf9
--- /dev/null
@@ -0,0 +1,8 @@
+<linker>
+  <assembly fullname="System.Text.Json">
+    <type fullname="System.Text.Json.JsonSerializer">
+      <method signature="System.Boolean get_IsReflectionEnabledByDefault()" body="stub" value="false" 
+              feature="System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault" featurevalue="false"/>
+    </type>
+  </assembly>
+</linker>
\ No newline at end of file
index 03734cf..261abe0 100644 (file)
@@ -19,6 +19,10 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
   </PropertyGroup>
 
   <ItemGroup>
+    <ILLinkSubstitutionsXmls Include="ILLink\ILLink.Substitutions.xml" />
+  </ItemGroup>
+
+  <ItemGroup>
     <Compile Include="$(CommonPath)System\HexConverter.cs" Link="Common\System\HexConverter.cs" />
     <Compile Include="$(CommonPath)System\Text\Json\PooledByteBufferWriter.cs" Link="Common\System\Text\Json\PooledByteBufferWriter.cs" />
     <Compile Include="..\Common\JsonCamelCaseNamingPolicy.cs" Link="Common\System\Text\Json\JsonCamelCaseNamingPolicy.cs" />
index 9c028f0..3e1291c 100644 (file)
@@ -5,9 +5,7 @@ namespace System.Text.Json
 {
     internal static class AppContextSwitchHelper
     {
-        public static bool IsSourceGenReflectionFallbackEnabled => s_isSourceGenReflectionFallbackEnabled;
-
-        private static readonly bool s_isSourceGenReflectionFallbackEnabled =
+        public static bool IsSourceGenReflectionFallbackEnabled { get; } =
             AppContext.TryGetSwitch(
                 switchName: "System.Text.Json.Serialization.EnableSourceGenReflectionFallback",
                 isEnabled: out bool value)
index b2815ce..3f20288 100644 (file)
@@ -13,6 +13,20 @@ namespace System.Text.Json
         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.";
         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.";
 
+        /// <summary>
+        /// Indicates whether unconfigured <see cref="JsonSerializerOptions"/> instances
+        /// should be set to use the reflection-based <see cref="DefaultJsonTypeInfoResolver"/>.
+        /// </summary>
+        /// <remarks>
+        /// The value of the property is backed by the "System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault"
+        /// <see cref="AppContext"/> setting and defaults to <see langword="true"/> if unset.
+        /// </remarks>
+        public static bool IsReflectionEnabledByDefault { get; } =
+            AppContext.TryGetSwitch(
+                switchName: "System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault",
+                isEnabled: out bool value)
+            ? value : true;
+
         [RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)]
         [RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)]
         private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type inputType, bool fallBackToNearestAncestorType = false)
@@ -21,9 +35,9 @@ namespace System.Text.Json
 
             options ??= JsonSerializerOptions.Default;
 
-            if (!options.IsInitializedForReflectionSerializer)
+            if (!options.IsConfiguredForJsonSerializer)
             {
-                options.InitializeForReflectionSerializer();
+                options.ConfigureForJsonSerializer();
             }
 
             // In order to improve performance of polymorphic root-level object serialization,
index 5824177..07532b8 100644 (file)
@@ -9,7 +9,7 @@ namespace System.Text.Json.Serialization
     /// <summary>
     /// Provides metadata about a set of types that is relevant to JSON serialization.
     /// </summary>
-    public abstract partial class JsonSerializerContext : IJsonTypeInfoResolver
+    public abstract partial class JsonSerializerContext : IJsonTypeInfoResolver, IBuiltInJsonTypeInfoResolver
     {
         private JsonSerializerOptions? _options;
 
@@ -49,7 +49,7 @@ namespace System.Text.Json.Serialization
         /// Indicates whether pre-generated serialization logic for types in the context
         /// is compatible with the run time specified <see cref="JsonSerializerOptions"/>.
         /// </summary>
-        internal bool IsCompatibleWithGeneratedOptions(JsonSerializerOptions options)
+        bool IBuiltInJsonTypeInfoResolver.IsCompatibleWithOptions(JsonSerializerOptions options)
         {
             Debug.Assert(options != null);
 
index fe2f658..b6c5019 100644 (file)
@@ -45,7 +45,7 @@ namespace System.Text.Json
                 ThrowHelper.ThrowArgumentNullException(nameof(typeToConvert));
             }
 
-            if (_typeInfoResolver is null)
+            if (JsonSerializer.IsReflectionEnabledByDefault && _typeInfoResolver is null)
             {
                 // Backward compatibility -- root & query the default reflection converters
                 // but do not populate the TypeInfoResolver setting.
index 8014363..5359499 100644 (file)
@@ -130,19 +130,6 @@ namespace System.Text.Json
             TrackOptionsInstance(this);
         }
 
-        /// <summary>Tracks the options instance to enable all instances to be enumerated.</summary>
-        private static void TrackOptionsInstance(JsonSerializerOptions options) => TrackedOptionsInstances.All.Add(options, null);
-
-        internal static class TrackedOptionsInstances
-        {
-            /// <summary>Tracks all live JsonSerializerOptions instances.</summary>
-            /// <remarks>Instances are added to the table in their constructor.</remarks>
-            public static ConditionalWeakTable<JsonSerializerOptions, object?> All { get; } =
-                // TODO https://github.com/dotnet/runtime/issues/51159:
-                // Look into linking this away / disabling it when hot reload isn't in use.
-                new ConditionalWeakTable<JsonSerializerOptions, object?>();
-        }
-
         /// <summary>
         /// Constructs a new <see cref="JsonSerializerOptions"/> instance with a predefined set of options determined by the specified <see cref="JsonSerializerDefaults"/>.
         /// </summary>
@@ -161,6 +148,19 @@ namespace System.Text.Json
             }
         }
 
+        /// <summary>Tracks the options instance to enable all instances to be enumerated.</summary>
+        private static void TrackOptionsInstance(JsonSerializerOptions options) => TrackedOptionsInstances.All.Add(options, null);
+
+        internal static class TrackedOptionsInstances
+        {
+            /// <summary>Tracks all live JsonSerializerOptions instances.</summary>
+            /// <remarks>Instances are added to the table in their constructor.</remarks>
+            public static ConditionalWeakTable<JsonSerializerOptions, object?> All { get; } =
+                // TODO https://github.com/dotnet/runtime/issues/51159:
+                // Look into linking this away / disabling it when hot reload isn't in use.
+                new ConditionalWeakTable<JsonSerializerOptions, object?>();
+        }
+
         /// <summary>
         /// Binds current <see cref="JsonSerializerOptions"/> instance with a new instance of the specified <see cref="Serialization.JsonSerializerContext"/> type.
         /// </summary>
@@ -638,32 +638,7 @@ namespace System.Text.Json
             {
                 Debug.Assert(IsReadOnly);
                 Debug.Assert(TypeInfoResolver != null);
-                return _canUseFastPathSerializationLogic ??= CanUseFastPath(TypeInfoResolver);
-
-                bool CanUseFastPath(IJsonTypeInfoResolver resolver)
-                {
-                    switch (resolver)
-                    {
-                        case DefaultJsonTypeInfoResolver defaultResolver:
-                            return defaultResolver.GetType() == typeof(DefaultJsonTypeInfoResolver) &&
-                                   defaultResolver.Modifiers.Count == 0;
-                        case JsonSerializerContext ctx:
-                            return ctx.IsCompatibleWithGeneratedOptions(this);
-                        case JsonTypeInfoResolverChain resolverChain:
-                            foreach (IJsonTypeInfoResolver component in resolverChain)
-                            {
-                                if (!CanUseFastPath(component))
-                                {
-                                    return false;
-                                }
-                            }
-
-                            return true;
-
-                        default:
-                            return false;
-                    }
-                }
+                return _canUseFastPathSerializationLogic ??= TypeInfoResolver.IsCompatibleWithOptions(this);
             }
         }
 
@@ -699,35 +674,38 @@ namespace System.Text.Json
         }
 
         /// <summary>
-        /// Initializes the converters for the reflection-based serializer.
+        /// Configures the instance for use by the JsonSerializer APIs.
         /// </summary>
         [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
         [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
-        internal void InitializeForReflectionSerializer()
+        internal void ConfigureForJsonSerializer()
         {
-            // Even if a resolver has already been specified, we need to root
-            // the default resolver to gain access to the default converters.
-            DefaultJsonTypeInfoResolver defaultResolver = DefaultJsonTypeInfoResolver.RootDefaultInstance();
-
-            switch (_typeInfoResolver)
+            if (JsonSerializer.IsReflectionEnabledByDefault)
             {
-                case null:
-                    // Use the default reflection-based resolver if no resolver has been specified.
-                    _typeInfoResolver = defaultResolver;
-                    break;
+                // Even if a resolver has already been specified, we need to root
+                // the default resolver to gain access to the default converters.
+                DefaultJsonTypeInfoResolver defaultResolver = DefaultJsonTypeInfoResolver.RootDefaultInstance();
 
-                case JsonSerializerContext ctx when AppContextSwitchHelper.IsSourceGenReflectionFallbackEnabled:
-                    // .NET 6 compatibility mode: enable fallback to reflection metadata for JsonSerializerContext
-                    _effectiveJsonTypeInfoResolver = JsonTypeInfoResolver.Combine(ctx, defaultResolver);
-                    break;
+                switch (_typeInfoResolver)
+                {
+                    case null:
+                        // Use the default reflection-based resolver if no resolver has been specified.
+                        _typeInfoResolver = defaultResolver;
+                        break;
+
+                    case JsonSerializerContext ctx when AppContextSwitchHelper.IsSourceGenReflectionFallbackEnabled:
+                        // .NET 6 compatibility mode: enable fallback to reflection metadata for JsonSerializerContext
+                        _effectiveJsonTypeInfoResolver = JsonTypeInfoResolver.Combine(ctx, defaultResolver);
+                        break;
+                }
             }
 
             MakeReadOnly();
-            _isInitializedForReflectionSerializer = true;
+            _isConfiguredForJsonSerializer = true;
         }
 
-        internal bool IsInitializedForReflectionSerializer => _isInitializedForReflectionSerializer;
-        private volatile bool _isInitializedForReflectionSerializer;
+        internal bool IsConfiguredForJsonSerializer => _isConfiguredForJsonSerializer;
+        private volatile bool _isConfiguredForJsonSerializer;
 
         // Only populated in .NET 6 compatibility mode encoding reflection fallback in source gen
         private IJsonTypeInfoResolver? _effectiveJsonTypeInfoResolver;
@@ -852,8 +830,15 @@ namespace System.Text.Json
         {
             var options = new JsonSerializerOptions
             {
-                TypeInfoResolver = DefaultJsonTypeInfoResolver.RootDefaultInstance(),
-                _isReadOnly = true
+                // Because we're marking the default instance as read-only,
+                // we need to specify a resolver instance for the case where
+                // reflection is disabled by default: use one that returns null for all types.
+
+                TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault
+                    ? DefaultJsonTypeInfoResolver.RootDefaultInstance()
+                    : new JsonTypeInfoResolverChain(),
+
+                _isReadOnly = true,
             };
 
             return Interlocked.CompareExchange(ref s_defaultOptions, options, null) ?? options;
index 2d746e0..8c01476 100644 (file)
@@ -399,13 +399,13 @@ namespace System.Text.Json.Serialization.Metadata
                     MethodInfo? getMethod = propertyInfo.GetMethod;
                     if (getMethod != null && (getMethod.IsPublic || useNonPublicAccessors))
                     {
-                        jsonPropertyInfo.Get = DefaultJsonTypeInfoResolver.MemberAccessor.CreatePropertyGetter<T>(propertyInfo);
+                        jsonPropertyInfo.Get = MemberAccessor.CreatePropertyGetter<T>(propertyInfo);
                     }
 
                     MethodInfo? setMethod = propertyInfo.SetMethod;
                     if (setMethod != null && (setMethod.IsPublic || useNonPublicAccessors))
                     {
-                        jsonPropertyInfo.Set = DefaultJsonTypeInfoResolver.MemberAccessor.CreatePropertySetter<T>(propertyInfo);
+                        jsonPropertyInfo.Set = MemberAccessor.CreatePropertySetter<T>(propertyInfo);
                     }
 
                     break;
@@ -413,11 +413,11 @@ namespace System.Text.Json.Serialization.Metadata
                 case FieldInfo fieldInfo:
                     Debug.Assert(fieldInfo.IsPublic);
 
-                    jsonPropertyInfo.Get = DefaultJsonTypeInfoResolver.MemberAccessor.CreateFieldGetter<T>(fieldInfo);
+                    jsonPropertyInfo.Get = MemberAccessor.CreateFieldGetter<T>(fieldInfo);
 
                     if (!fieldInfo.IsInitOnly)
                     {
-                        jsonPropertyInfo.Set = DefaultJsonTypeInfoResolver.MemberAccessor.CreateFieldSetter<T>(fieldInfo);
+                        jsonPropertyInfo.Set = MemberAccessor.CreateFieldSetter<T>(fieldInfo);
                     }
 
                     break;
index 5d00959..1a16f8d 100644 (file)
@@ -13,7 +13,7 @@ namespace System.Text.Json.Serialization.Metadata
     /// <remarks>
     /// The contract resolver used by <see cref="JsonSerializerOptions.Default"/>.
     /// </remarks>
-    public partial class DefaultJsonTypeInfoResolver : IJsonTypeInfoResolver
+    public partial class DefaultJsonTypeInfoResolver : IJsonTypeInfoResolver, IBuiltInJsonTypeInfoResolver
     {
         private bool _mutable;
 
@@ -122,6 +122,11 @@ namespace System.Text.Json.Serialization.Metadata
             }
         }
 
+        bool IBuiltInJsonTypeInfoResolver.IsCompatibleWithOptions(JsonSerializerOptions _)
+            // Metadata generated by the default resolver is compatible by definition,
+            // provided that no user extensions have been made on the class.
+            => _modifiers is null or { Count: 0 } && GetType() == typeof(DefaultJsonTypeInfoResolver);
+
         internal static bool IsDefaultInstanceRooted => s_defaultInstance is not null;
         private static DefaultJsonTypeInfoResolver? s_defaultInstance;
 
index 8ab34bd..93c464b 100644 (file)
@@ -695,12 +695,7 @@ namespace System.Text.Json.Serialization.Metadata
                     return false;
                 }
 
-                return OriginatingResolver switch
-                {
-                    JsonSerializerContext ctx => ctx.IsCompatibleWithGeneratedOptions(Options),
-                    DefaultJsonTypeInfoResolver => true, // generates default contracts by definition
-                    _ => false
-                };
+                return OriginatingResolver.IsCompatibleWithOptions(Options);
             }
         }
 
index f7c6e12..d890b9b 100644 (file)
@@ -38,5 +38,25 @@ namespace System.Text.Json.Serialization.Metadata
 
             return resolverChain.Count == 1 ? resolverChain[0] : resolverChain;
         }
+
+        /// <summary>
+        /// Indicates whether the metadata generated by the current resolver
+        /// are compatible with the run time specified <see cref="JsonSerializerOptions"/>.
+        /// </summary>
+        internal static bool IsCompatibleWithOptions(this IJsonTypeInfoResolver? resolver, JsonSerializerOptions options)
+            => resolver is IBuiltInJsonTypeInfoResolver bir && bir.IsCompatibleWithOptions(options);
+    }
+
+    /// <summary>
+    /// Implemented by the built-in converters to avoid rooting
+    /// unused resolver dependencies in the context of the trimmer.
+    /// </summary>
+    internal interface IBuiltInJsonTypeInfoResolver
+    {
+        /// <summary>
+        /// Indicates whether the metadata generated by the current resolver
+        /// are compatible with the run time specified <see cref="JsonSerializerOptions"/>.
+        /// </summary>
+        bool IsCompatibleWithOptions(JsonSerializerOptions options);
     }
 }
index 26f2292..e2a5664 100644 (file)
@@ -6,7 +6,7 @@ using System.Diagnostics;
 namespace System.Text.Json.Serialization.Metadata
 {
     [DebuggerDisplay("{DebuggerDisplay,nq}")]
-    internal class JsonTypeInfoResolverChain : ConfigurationList<IJsonTypeInfoResolver>, IJsonTypeInfoResolver
+    internal class JsonTypeInfoResolverChain : ConfigurationList<IJsonTypeInfoResolver>, IJsonTypeInfoResolver, IBuiltInJsonTypeInfoResolver
     {
         public JsonTypeInfoResolverChain() : base(null) { }
         public override bool IsReadOnly => true;
@@ -44,6 +44,19 @@ namespace System.Text.Json.Serialization.Metadata
             }
         }
 
+        bool IBuiltInJsonTypeInfoResolver.IsCompatibleWithOptions(JsonSerializerOptions options)
+        {
+            foreach (IJsonTypeInfoResolver component in _list)
+            {
+                if (!component.IsCompatibleWithOptions(options))
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
         internal string DebuggerDisplay
         {
             get
index 410ef93..d290fd8 100644 (file)
@@ -481,6 +481,120 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(unsupportedValue, options));
         }
 
+        [Fact]
+        public static void JsonSerializer_IsReflectionEnabledByDefault_DefaultsToTrue()
+        {
+            Assert.True(JsonSerializer.IsReflectionEnabledByDefault);
+        }
+
+        [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public static void Options_DisablingIsReflectionEnabledByDefaultSwitch_DefaultOptionsDoesNotSupportReflection()
+        {
+            var options = new RemoteInvokeOptions
+            {
+                RuntimeConfigurationOptions =
+                {
+                    ["System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault"] = false
+                }
+            };
+
+            RemoteExecutor.Invoke(static () =>
+            {
+                Assert.False(JsonSerializer.IsReflectionEnabledByDefault);
+
+                var options = JsonSerializerOptions.Default;
+                Assert.True(options.IsReadOnly);
+
+                Assert.NotNull(options.TypeInfoResolver);
+                Assert.True(options.TypeInfoResolver is not DefaultJsonTypeInfoResolver);
+                IList<IJsonTypeInfoResolver> resolverList = Assert.IsAssignableFrom<IList<IJsonTypeInfoResolver>>(options.TypeInfoResolver);
+
+                Assert.Empty(resolverList);
+                Assert.Empty(options.TypeInfoResolverChain);
+
+                Assert.Throws<NotSupportedException>(() => options.GetTypeInfo(typeof(string)));
+                Assert.Throws<NotSupportedException>(() => options.GetConverter(typeof(string)));
+
+                Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize("string"));
+                Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize("string", options));
+
+                Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<string>("\"string\""));
+                Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<string>("\"string\"", options));
+
+            }, options).Dispose();
+        }
+
+        [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public static void Options_DisablingIsReflectionEnabledByDefaultSwitch_NewOptionsDoesNotSupportReflection()
+        {
+            var options = new RemoteInvokeOptions
+            {
+                RuntimeConfigurationOptions =
+                {
+                    ["System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault"] = false
+                }
+            };
+
+            RemoteExecutor.Invoke(static () =>
+            {
+                Assert.False(JsonSerializer.IsReflectionEnabledByDefault);
+
+                var options = new JsonSerializerOptions();
+                Assert.False(options.IsReadOnly);
+
+                Assert.Null(options.TypeInfoResolver);
+                Assert.Empty(options.TypeInfoResolverChain);
+
+                Assert.Throws<NotSupportedException>(() => options.GetTypeInfo(typeof(string)));
+                Assert.Throws<NotSupportedException>(() => options.GetConverter(typeof(string)));
+
+                Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize("string", options));
+                Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<string>("\"string\"", options));
+
+                Assert.False(options.IsReadOnly); // failed operations should not lock the instance
+
+                // Can still use reflection via explicit configuration
+                options.TypeInfoResolver = new DefaultJsonTypeInfoResolver();
+                Assert.Equal(new[] { options.TypeInfoResolver }, options.TypeInfoResolverChain);
+
+                Assert.NotNull(options.GetTypeInfo(typeof(string)));
+                Assert.NotNull(options.GetConverter(typeof(string)));
+
+                string json = JsonSerializer.Serialize("string", options);
+                string value = JsonSerializer.Deserialize<string>(json, options);
+                Assert.Equal("string", value);
+
+            }, options).Dispose();
+        }
+
+        [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public static void Options_DisablingIsReflectionEnabledByDefaultSwitch_CanUseSourceGen()
+        {
+            var options = new RemoteInvokeOptions
+            {
+                RuntimeConfigurationOptions =
+                {
+                    ["System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault"] = false
+                }
+            };
+
+            RemoteExecutor.Invoke(static () =>
+            {
+                Assert.False(JsonSerializer.IsReflectionEnabledByDefault);
+
+                var options = new JsonSerializerOptions();
+                options.TypeInfoResolverChain.Add(JsonContext.Default);
+
+                string json = JsonSerializer.Serialize(new WeatherForecastWithPOCOs(), options);
+                WeatherForecastWithPOCOs result = JsonSerializer.Deserialize<WeatherForecastWithPOCOs>(json, options);
+                Assert.NotNull(result);
+
+            }, options).Dispose();
+        }
+
         [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
         [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
         [InlineData(false)]
@@ -550,6 +664,33 @@ namespace System.Text.Json.Serialization.Tests
             }, options).Dispose();
         }
 
+        [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public static void Options_JsonSerializerContext_Net6CompatibilitySwitch_IsOverriddenByDisablingIsReflectionEnabledByDefault()
+        {
+            var options = new RemoteInvokeOptions
+            {
+                RuntimeConfigurationOptions =
+                {
+                    ["System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault"] = false,
+                    ["System.Text.Json.Serialization.EnableSourceGenReflectionFallback"] = true
+                }
+            };
+
+            RemoteExecutor.Invoke(static () =>
+            {
+                Assert.False(JsonSerializer.IsReflectionEnabledByDefault);
+
+                JsonContext context = JsonContext.Default;
+                var unsupportedValue = new MyClass();
+
+                Assert.Null(context.GetTypeInfo(typeof(MyClass)));
+                Assert.Throws<NotSupportedException>(() => context.Options.GetConverter(typeof(MyClass)));
+                Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(unsupportedValue, context.Options));
+
+            }, options).Dispose();
+        }
+
         [Fact]
         public static void Options_JsonSerializerContext_Combine_FallbackToReflection()
         {
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/TrimmingTests/IsReflectionEnabledByDefaultFalse.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/TrimmingTests/IsReflectionEnabledByDefaultFalse.cs
new file mode 100644 (file)
index 0000000..e0db8f3
--- /dev/null
@@ -0,0 +1,146 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+
+#nullable enable
+
+public static class Program
+{
+    // Validates that expected the components are trimmed when
+    // the IsReflectionEnabledByDefault feature switch is turned off.
+    public static int Main()
+    {
+        MyPoco valueToSerialize = new MyPoco { Value = 42 };
+
+        // The default resolver should not surface DefaultJsonTypeInfoResolver.
+        if (JsonSerializerOptions.Default.TypeInfoResolver is not IList<IJsonTypeInfoResolver> { Count: 0 })
+        {
+            return -1;
+        }
+
+        // Serializing with options unset should throw NotSupportedException.
+        try
+        {
+            JsonSerializer.Serialize(valueToSerialize);
+            return -2;
+        }
+        catch (NotSupportedException)
+        {
+        }
+
+        // Serializing with default options unset should throw InvalidOperationException.
+        var options = new JsonSerializerOptions();
+        try
+        {
+            JsonSerializer.Serialize(valueToSerialize, options);
+            return -3;
+        }
+        catch (InvalidOperationException)
+        {
+        }
+
+        // Serializing with a custom resolver should work as expected.
+        options.TypeInfoResolver = new MyJsonResolver();
+        if (JsonSerializer.Serialize(valueToSerialize, options) != "{\"Value\":42}")
+        {
+            return -4;
+        }
+
+        // The Default resolver should have been trimmed from the application.
+        Type? reflectionResolver = GetJsonType("System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver");
+        if (reflectionResolver != null)
+        {
+            return -5;
+        }
+
+        return 100;
+    }
+
+    // The intention of this method is to ensure the trimmer doesn't preserve the Type.
+    private static Type? GetJsonType(string name) =>
+        typeof(JsonSerializer).Assembly.GetType(name, throwOnError: false);
+}
+
+public class MyPoco
+{
+    public int Value { get; set; }
+}
+
+public class MyJsonResolver : JsonSerializerContext, IJsonTypeInfoResolver
+{
+    public MyJsonResolver() : base(null) { }
+    protected override JsonSerializerOptions? GeneratedSerializerOptions => null;
+    public override JsonTypeInfo? GetTypeInfo(Type type) => GetTypeInfo(type, Options);
+
+    public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
+    {
+        if (type == typeof(int))
+        {
+            return Create_Int32(options);
+        }
+
+        if (type == typeof(MyPoco))
+        {
+            return Create_MyPoco(options);
+        }
+
+        return null;
+    }
+
+    private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::MyPoco> Create_MyPoco(global::System.Text.Json.JsonSerializerOptions options)
+    {
+        global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::MyPoco>? jsonTypeInfo = null;
+        global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<global::MyPoco> objectInfo = new global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<global::MyPoco>()
+        {
+            ObjectCreator = static () => new global::MyPoco(),
+            ObjectWithParameterizedConstructorCreator = null,
+            PropertyMetadataInitializer = _ => MyPocoPropInit(options),
+            ConstructorParameterMetadataInitializer = null,
+            NumberHandling = default,
+        };
+
+        jsonTypeInfo = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateObjectInfo<global::MyPoco>(options, objectInfo);
+        jsonTypeInfo.OriginatingResolver = this;
+        return jsonTypeInfo;
+    }
+
+    private static global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] MyPocoPropInit(global::System.Text.Json.JsonSerializerOptions options)
+    {
+        global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] properties = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[1];
+
+        global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Int32> info0 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Int32>()
+        {
+            IsProperty = true,
+            IsPublic = true,
+            IsVirtual = false,
+            DeclaringType = typeof(global::MyPoco),
+            Converter = null,
+            Getter = static (obj) => ((global::MyPoco)obj).Value,
+            Setter = static (obj, value) => ((global::MyPoco)obj).Value = value!,
+            IgnoreCondition = null,
+            HasJsonInclude = false,
+            IsExtensionData = false,
+            NumberHandling = default,
+            PropertyName = "Value",
+            JsonPropertyName = null
+        };
+
+        global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo propertyInfo0 = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<global::System.Int32>(options, info0);
+        properties[0] = propertyInfo0;
+
+        return properties;
+    }
+
+    private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Int32> Create_Int32(global::System.Text.Json.JsonSerializerOptions options)
+    {
+        global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Int32>? jsonTypeInfo = null;
+        jsonTypeInfo = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateValueInfo<global::System.Int32>(options, global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.Int32Converter);
+        jsonTypeInfo.OriginatingResolver = this;
+        return jsonTypeInfo;
+    }
+}
index 2f4a4e4..db59eac 100644 (file)
@@ -60,6 +60,9 @@
     <TestConsoleAppSourceFiles Include="EnumConverterTest.cs" />
     <TestConsoleAppSourceFiles Include="JsonConverterAttributeTest.cs" />
     <TestConsoleAppSourceFiles Include="StackOrQueueNotRootedTest.cs" />
+    <TestConsoleAppSourceFiles Include="IsReflectionEnabledByDefaultFalse.cs">
+      <DisabledFeatureSwitches>System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault</DisabledFeatureSwitches>
+    </TestConsoleAppSourceFiles>
   </ItemGroup>
 
   <Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Build.targets))" />