diff --git a/docs/workflow/trimming/feature-switches.md b/docs/workflow/trimming/feature-switches.md index 7600b3d7d480b..0aa44298f3c39 100644 --- a/docs/workflow/trimming/feature-switches.md +++ b/docs/workflow/trimming/feature-switches.md @@ -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 diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 12426171d3f1e..3002b59dcff3b 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -280,6 +280,7 @@ public static partial class JsonSerializer [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(ref System.Text.Json.Utf8JsonReader reader, System.Text.Json.JsonSerializerOptions? options = null) { throw null; } public static TValue? Deserialize(ref System.Text.Json.Utf8JsonReader reader, System.Text.Json.Serialization.Metadata.JsonTypeInfo 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 index 0000000000000..026bbf9f13602 --- /dev/null +++ b/src/libraries/System.Text.Json/src/ILLink/ILLink.Substitutions.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 03734cf251116..261abe0130e79 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -18,6 +18,10 @@ The System.Text.Json library is built-in as part of the shared framework in .NET $(NoWarn);nullable + + + + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs index 9c028f0216517..3e1291c676d92 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs @@ -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) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs index b2815ce96c18e..3f202888853d5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs @@ -13,6 +13,20 @@ public static partial class JsonSerializer 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."; + /// + /// Indicates whether unconfigured instances + /// should be set to use the reflection-based . + /// + /// + /// The value of the property is backed by the "System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault" + /// setting and defaults to if unset. + /// + 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 @@ private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type inp options ??= JsonSerializerOptions.Default; - if (!options.IsInitializedForReflectionSerializer) + if (!options.IsConfiguredForJsonSerializer) { - options.InitializeForReflectionSerializer(); + options.ConfigureForJsonSerializer(); } // In order to improve performance of polymorphic root-level object serialization, diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs index 582417737f6db..07532b8c50ace 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs @@ -9,7 +9,7 @@ namespace System.Text.Json.Serialization /// /// Provides metadata about a set of types that is relevant to JSON serialization. /// - public abstract partial class JsonSerializerContext : IJsonTypeInfoResolver + public abstract partial class JsonSerializerContext : IJsonTypeInfoResolver, IBuiltInJsonTypeInfoResolver { private JsonSerializerOptions? _options; @@ -49,7 +49,7 @@ internal void AssociateWithOptions(JsonSerializerOptions options) /// Indicates whether pre-generated serialization logic for types in the context /// is compatible with the run time specified . /// - internal bool IsCompatibleWithGeneratedOptions(JsonSerializerOptions options) + bool IBuiltInJsonTypeInfoResolver.IsCompatibleWithOptions(JsonSerializerOptions options) { Debug.Assert(options != null); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index fe2f658579673..b6c501903a602 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -45,7 +45,7 @@ public JsonConverter GetConverter(Type typeToConvert) 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. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 801436338bea9..5359499e0cbea 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -130,19 +130,6 @@ public JsonSerializerOptions(JsonSerializerOptions options) TrackOptionsInstance(this); } - /// Tracks the options instance to enable all instances to be enumerated. - private static void TrackOptionsInstance(JsonSerializerOptions options) => TrackedOptionsInstances.All.Add(options, null); - - internal static class TrackedOptionsInstances - { - /// Tracks all live JsonSerializerOptions instances. - /// Instances are added to the table in their constructor. - public static ConditionalWeakTable 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(); - } - /// /// Constructs a new instance with a predefined set of options determined by the specified . /// @@ -161,6 +148,19 @@ public JsonSerializerOptions(JsonSerializerDefaults defaults) : this() } } + /// Tracks the options instance to enable all instances to be enumerated. + private static void TrackOptionsInstance(JsonSerializerOptions options) => TrackedOptionsInstances.All.Add(options, null); + + internal static class TrackedOptionsInstances + { + /// Tracks all live JsonSerializerOptions instances. + /// Instances are added to the table in their constructor. + public static ConditionalWeakTable 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(); + } + /// /// Binds current instance with a new instance of the specified type. /// @@ -638,32 +638,7 @@ internal bool CanUseFastPathSerializationLogic { 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 @@ public void MakeReadOnly() } /// - /// Initializes the converters for the reflection-based serializer. + /// Configures the instance for use by the JsonSerializer APIs. /// [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 @@ private static JsonSerializerOptions GetOrCreateDefaultOptionsInstance() { 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; 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 2d746e0b9c3d1..8c014763f0473 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 @@ -399,13 +399,13 @@ internal static void DeterminePropertyAccessors(JsonPropertyInfo jsonPrope MethodInfo? getMethod = propertyInfo.GetMethod; if (getMethod != null && (getMethod.IsPublic || useNonPublicAccessors)) { - jsonPropertyInfo.Get = DefaultJsonTypeInfoResolver.MemberAccessor.CreatePropertyGetter(propertyInfo); + jsonPropertyInfo.Get = MemberAccessor.CreatePropertyGetter(propertyInfo); } MethodInfo? setMethod = propertyInfo.SetMethod; if (setMethod != null && (setMethod.IsPublic || useNonPublicAccessors)) { - jsonPropertyInfo.Set = DefaultJsonTypeInfoResolver.MemberAccessor.CreatePropertySetter(propertyInfo); + jsonPropertyInfo.Set = MemberAccessor.CreatePropertySetter(propertyInfo); } break; @@ -413,11 +413,11 @@ internal static void DeterminePropertyAccessors(JsonPropertyInfo jsonPrope case FieldInfo fieldInfo: Debug.Assert(fieldInfo.IsPublic); - jsonPropertyInfo.Get = DefaultJsonTypeInfoResolver.MemberAccessor.CreateFieldGetter(fieldInfo); + jsonPropertyInfo.Get = MemberAccessor.CreateFieldGetter(fieldInfo); if (!fieldInfo.IsInitOnly) { - jsonPropertyInfo.Set = DefaultJsonTypeInfoResolver.MemberAccessor.CreateFieldSetter(fieldInfo); + jsonPropertyInfo.Set = MemberAccessor.CreateFieldSetter(fieldInfo); } break; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.cs index 5d00959a79a9a..1a16f8de807fb 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.cs @@ -13,7 +13,7 @@ namespace System.Text.Json.Serialization.Metadata /// /// The contract resolver used by . /// - public partial class DefaultJsonTypeInfoResolver : IJsonTypeInfoResolver + public partial class DefaultJsonTypeInfoResolver : IJsonTypeInfoResolver, IBuiltInJsonTypeInfoResolver { private bool _mutable; @@ -122,6 +122,11 @@ protected override void OnCollectionModifying() } } + 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; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index 8ab34bdb8875f..93c464be17737 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -695,12 +695,7 @@ bool IsCurrentNodeCompatible() return false; } - return OriginatingResolver switch - { - JsonSerializerContext ctx => ctx.IsCompatibleWithGeneratedOptions(Options), - DefaultJsonTypeInfoResolver => true, // generates default contracts by definition - _ => false - }; + return OriginatingResolver.IsCompatibleWithOptions(Options); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolver.cs index f7c6e125ce212..d890b9be94205 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolver.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolver.cs @@ -38,5 +38,25 @@ public static IJsonTypeInfoResolver Combine(params IJsonTypeInfoResolver?[] reso return resolverChain.Count == 1 ? resolverChain[0] : resolverChain; } + + /// + /// Indicates whether the metadata generated by the current resolver + /// are compatible with the run time specified . + /// + internal static bool IsCompatibleWithOptions(this IJsonTypeInfoResolver? resolver, JsonSerializerOptions options) + => resolver is IBuiltInJsonTypeInfoResolver bir && bir.IsCompatibleWithOptions(options); + } + + /// + /// Implemented by the built-in converters to avoid rooting + /// unused resolver dependencies in the context of the trimmer. + /// + internal interface IBuiltInJsonTypeInfoResolver + { + /// + /// Indicates whether the metadata generated by the current resolver + /// are compatible with the run time specified . + /// + bool IsCompatibleWithOptions(JsonSerializerOptions options); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolverChain.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolverChain.cs index 26f229210fbcd..e2a56646ff74c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolverChain.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoResolverChain.cs @@ -6,7 +6,7 @@ namespace System.Text.Json.Serialization.Metadata { [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal class JsonTypeInfoResolverChain : ConfigurationList, IJsonTypeInfoResolver + internal class JsonTypeInfoResolverChain : ConfigurationList, IJsonTypeInfoResolver, IBuiltInJsonTypeInfoResolver { public JsonTypeInfoResolverChain() : base(null) { } public override bool IsReadOnly => true; @@ -44,6 +44,19 @@ internal void AddFlattened(IJsonTypeInfoResolver? resolver) } } + bool IBuiltInJsonTypeInfoResolver.IsCompatibleWithOptions(JsonSerializerOptions options) + { + foreach (IJsonTypeInfoResolver component in _list) + { + if (!component.IsCompatibleWithOptions(options)) + { + return false; + } + } + + return true; + } + internal string DebuggerDisplay { get diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs index 410ef93459438..d290fd8d259bb 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs @@ -481,6 +481,120 @@ public static void Options_JsonSerializerContext_DoesNotFallbackToReflection() Assert.Throws(() => 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 resolverList = Assert.IsAssignableFrom>(options.TypeInfoResolver); + + Assert.Empty(resolverList); + Assert.Empty(options.TypeInfoResolverChain); + + Assert.Throws(() => options.GetTypeInfo(typeof(string))); + Assert.Throws(() => options.GetConverter(typeof(string))); + + Assert.Throws(() => JsonSerializer.Serialize("string")); + Assert.Throws(() => JsonSerializer.Serialize("string", options)); + + Assert.Throws(() => JsonSerializer.Deserialize("\"string\"")); + Assert.Throws(() => JsonSerializer.Deserialize("\"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(() => options.GetTypeInfo(typeof(string))); + Assert.Throws(() => options.GetConverter(typeof(string))); + + Assert.Throws(() => JsonSerializer.Serialize("string", options)); + Assert.Throws(() => JsonSerializer.Deserialize("\"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(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(json, options); + Assert.NotNull(result); + + }, options).Dispose(); + } + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] [InlineData(false)] @@ -550,6 +664,33 @@ public static void Options_JsonSerializerContext_Net6CompatibilitySwitch_FallsBa }, 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(() => context.Options.GetConverter(typeof(MyClass))); + Assert.Throws(() => 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 index 0000000000000..e0db8f39b080d --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/TrimmingTests/IsReflectionEnabledByDefaultFalse.cs @@ -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 { 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 Create_MyPoco(global::System.Text.Json.JsonSerializerOptions options) + { + global::System.Text.Json.Serialization.Metadata.JsonTypeInfo? jsonTypeInfo = null; + global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues objectInfo = new global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues() + { + ObjectCreator = static () => new global::MyPoco(), + ObjectWithParameterizedConstructorCreator = null, + PropertyMetadataInitializer = _ => MyPocoPropInit(options), + ConstructorParameterMetadataInitializer = null, + NumberHandling = default, + }; + + jsonTypeInfo = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateObjectInfo(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 info0 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues() + { + 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(options, info0); + properties[0] = propertyInfo0; + + return properties; + } + + private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo Create_Int32(global::System.Text.Json.JsonSerializerOptions options) + { + global::System.Text.Json.Serialization.Metadata.JsonTypeInfo? jsonTypeInfo = null; + jsonTypeInfo = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateValueInfo(options, global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.Int32Converter); + jsonTypeInfo.OriginatingResolver = this; + return jsonTypeInfo; + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/TrimmingTests/System.Text.Json.TrimmingTests.proj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/TrimmingTests/System.Text.Json.TrimmingTests.proj index 2f4a4e4a33687..db59eac29e719 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/TrimmingTests/System.Text.Json.TrimmingTests.proj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/TrimmingTests/System.Text.Json.TrimmingTests.proj @@ -60,6 +60,9 @@ + + System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault +