* 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>
| 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
[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.")]
--- /dev/null
+<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
</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" />
{
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)
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)
options ??= JsonSerializerOptions.Default;
- if (!options.IsInitializedForReflectionSerializer)
+ if (!options.IsConfiguredForJsonSerializer)
{
- options.InitializeForReflectionSerializer();
+ options.ConfigureForJsonSerializer();
}
// In order to improve performance of polymorphic root-level object 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;
/// 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);
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.
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>
}
}
+ /// <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>
{
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);
}
}
}
/// <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;
{
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;
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;
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;
/// <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;
}
}
+ 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;
return false;
}
- return OriginatingResolver switch
- {
- JsonSerializerContext ctx => ctx.IsCompatibleWithGeneratedOptions(Options),
- DefaultJsonTypeInfoResolver => true, // generates default contracts by definition
- _ => false
- };
+ return OriginatingResolver.IsCompatibleWithOptions(Options);
}
}
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);
}
}
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;
}
}
+ bool IBuiltInJsonTypeInfoResolver.IsCompatibleWithOptions(JsonSerializerOptions options)
+ {
+ foreach (IJsonTypeInfoResolver component in _list)
+ {
+ if (!component.IsCompatibleWithOptions(options))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
internal string DebuggerDisplay
{
get
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)]
}, 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()
{
--- /dev/null
+// 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;
+ }
+}
<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))" />