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 a6b71ea822077..c5d44694c7193 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 @@ -13,6 +13,9 @@ namespace System.Text.Json.Serialization.Metadata { public partial class DefaultJsonTypeInfoResolver { + private static readonly bool s_isNullabilityInfoContextSupported = + AppContext.TryGetSwitch("System.Reflection.NullabilityInfoContext.IsSupported", out bool isSupported) ? isSupported : true; + internal static MemberAccessor MemberAccessor { [RequiresUnreferencedCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] @@ -71,7 +74,10 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte if (typeInfo.Kind is JsonTypeInfoKind.Object) { - NullabilityInfoContext nullabilityCtx = new(); + // If the System.Reflection.NullabilityInfoContext.IsSupported feature switch has been disabled, + // we want to avoid resolving nullability information for properties and parameters unless the + // user has explicitly opted into nullability enforcement in which case an exception will be surfaced. + NullabilityInfoContext? nullabilityCtx = s_isNullabilityInfoContextSupported || options.RespectNullableAnnotations ? new() : null; if (converter.ConstructorIsParameterized) { @@ -91,7 +97,7 @@ private static JsonTypeInfo CreateTypeInfoCore(Type type, JsonConverter converte [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - private static void PopulateProperties(JsonTypeInfo typeInfo, NullabilityInfoContext nullabilityCtx) + private static void PopulateProperties(JsonTypeInfo typeInfo, NullabilityInfoContext? nullabilityCtx) { Debug.Assert(!typeInfo.IsReadOnly); Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object); @@ -158,7 +164,7 @@ private static void PopulateProperties(JsonTypeInfo typeInfo, NullabilityInfoCon private static void AddMembersDeclaredBySuperType( JsonTypeInfo typeInfo, Type currentType, - NullabilityInfoContext nullabilityCtx, + NullabilityInfoContext? nullabilityCtx, bool constructorHasSetsRequiredMembersAttribute, ref JsonTypeInfo.PropertyHierarchyResolutionState state) { @@ -219,7 +225,7 @@ private static void AddMember( JsonTypeInfo typeInfo, Type typeToConvert, MemberInfo memberInfo, - NullabilityInfoContext nullabilityCtx, + NullabilityInfoContext? nullabilityCtx, bool shouldCheckForRequiredKeyword, bool hasJsonIncludeAttribute, ref JsonTypeInfo.PropertyHierarchyResolutionState state) @@ -241,7 +247,7 @@ private static void AddMember( JsonTypeInfo typeInfo, Type typeToConvert, MemberInfo memberInfo, - NullabilityInfoContext nullabilityCtx, + NullabilityInfoContext? nullabilityCtx, JsonSerializerOptions options, bool shouldCheckForRequiredKeyword, bool hasJsonIncludeAttribute) @@ -301,7 +307,7 @@ private static bool PropertyIsOverriddenAndIgnored(PropertyInfo propertyInfo, Di [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo, NullabilityInfoContext nullabilityCtx) + private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo, NullabilityInfoContext? nullabilityCtx) { Debug.Assert(typeInfo.Converter.ConstructorInfo != null); ParameterInfo[] parameters = typeInfo.Converter.ConstructorInfo.GetParameters(); @@ -326,9 +332,7 @@ private static void PopulateParameterInfoValues(JsonTypeInfo typeInfo, Nullabili Position = reflectionInfo.Position, HasDefaultValue = reflectionInfo.HasDefaultValue, DefaultValue = reflectionInfo.GetDefaultValue(), - IsNullable = - reflectionInfo.ParameterType.IsNullableType() && - DetermineParameterNullability(reflectionInfo, nullabilityCtx) is not NullabilityState.NotNull, + IsNullable = DetermineParameterNullability(reflectionInfo, nullabilityCtx) is not NullabilityState.NotNull, }; jsonParameters[i] = jsonInfo; @@ -344,7 +348,7 @@ private static void PopulatePropertyInfo( MemberInfo memberInfo, JsonConverter? customConverter, JsonIgnoreCondition? ignoreCondition, - NullabilityInfoContext nullabilityCtx, + NullabilityInfoContext? nullabilityCtx, bool shouldCheckForRequiredKeyword, bool hasJsonIncludeAttribute) { @@ -487,9 +491,9 @@ internal static void DeterminePropertyAccessors(JsonPropertyInfo jsonPrope [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, MemberInfo memberInfo, NullabilityInfoContext nullabilityCtx) + private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, MemberInfo memberInfo, NullabilityInfoContext? nullabilityCtx) { - if (!propertyInfo.PropertyTypeCanBeNull) + if (!propertyInfo.PropertyTypeCanBeNull || nullabilityCtx is null) { return; } @@ -511,8 +515,17 @@ private static void DeterminePropertyNullability(JsonPropertyInfo propertyInfo, [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - private static NullabilityState DetermineParameterNullability(ParameterInfo parameterInfo, NullabilityInfoContext nullabilityCtx) + private static NullabilityState DetermineParameterNullability(ParameterInfo parameterInfo, NullabilityInfoContext? nullabilityCtx) { + if (!parameterInfo.ParameterType.IsNullableType()) + { + return NullabilityState.NotNull; + } + + if (nullabilityCtx is null) + { + return NullabilityState.Unknown; + } #if NET8_0 // Workaround for https://github.com/dotnet/runtime/issues/92487 // The fix has been incorporated into .NET 9 (and the polyfilled implementations in netfx). 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 3ada5243181d7..f5ef9964d8f1c 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 @@ -960,6 +960,60 @@ public static void Options_RespectNullableAnnotationsDefault_FeatureSwitch(bool? }, arg, options).Dispose(); } + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void Options_NullabilityInfoFeatureSwitchDisabled_ReportsPropertiesAsNullable() + { + var options = new RemoteInvokeOptions() + { + RuntimeConfigurationOptions = + { + ["System.Reflection.NullabilityInfoContext.IsSupported"] = false + } + }; + + RemoteExecutor.Invoke(static () => + { + var value = new NullableAnnotationsTests.NotNullablePropertyClass(); + string expectedJson = """{"Property":null}"""; + + Assert.Null(value.Property); + string json = JsonSerializer.Serialize(value); + Assert.Equal(expectedJson, json); + value = JsonSerializer.Deserialize(json); + Assert.Null(value.Property); + + }, options).Dispose(); + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void Options_NullabilityInfoFeatureSwitchDisabled_RespectNullabilityAnnotationsEnabled_ThrowsInvalidOperationException() + { + var options = new RemoteInvokeOptions() + { + RuntimeConfigurationOptions = + { + ["System.Reflection.NullabilityInfoContext.IsSupported"] = false + } + }; + + RemoteExecutor.Invoke(static () => + { + var jsonOptions = new JsonSerializerOptions { RespectNullableAnnotations = true }; + var value = new NullableAnnotationsTests.NotNullablePropertyClass(); + string expectedJson = """{"Property":null}"""; + InvalidOperationException ex; + + ex = Assert.Throws(() => JsonSerializer.Serialize(value, jsonOptions)); + Assert.Contains("System.Reflection.NullabilityInfoContext.IsSupported", ex.Message); + + ex = Assert.Throws(() => JsonSerializer.Deserialize(expectedJson, jsonOptions)); + Assert.Contains("System.Reflection.NullabilityInfoContext.IsSupported", ex.Message); + + }, options).Dispose(); + } + private static void GenericObjectOrJsonElementConverterTestHelper(string converterName, object objectValue, string stringValue) { var options = new JsonSerializerOptions();