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 e17ce01c77035..8cd779f5a0218 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -36,6 +36,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + 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 new file mode 100644 index 0000000000000..9c028f0216517 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json +{ + internal static class AppContextSwitchHelper + { + public static bool IsSourceGenReflectionFallbackEnabled => s_isSourceGenReflectionFallbackEnabled; + + private static readonly bool s_isSourceGenReflectionFallbackEnabled = + AppContext.TryGetSwitch( + switchName: "System.Text.Json.Serialization.EnableSourceGenReflectionFallback", + isEnabled: out bool value) + ? value : false; + } +} 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 596a9c37bae48..fa543671792fb 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 @@ -628,7 +628,20 @@ internal void InitializeForReflectionSerializer() // 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(); - _typeInfoResolver ??= defaultResolver; + + 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; + } + IsImmutable = true; _isInitializedForReflectionSerializer = true; } @@ -636,6 +649,9 @@ internal void InitializeForReflectionSerializer() internal bool IsInitializedForReflectionSerializer => _isInitializedForReflectionSerializer; private volatile bool _isInitializedForReflectionSerializer; + // Only populated in .NET 6 compatibility mode encoding reflection fallback in source gen + private IJsonTypeInfoResolver? _effectiveJsonTypeInfoResolver; + internal void InitializeForMetadataGeneration() { if (_typeInfoResolver is null) @@ -648,7 +664,7 @@ internal void InitializeForMetadataGeneration() private JsonTypeInfo? GetTypeInfoNoCaching(Type type) { - JsonTypeInfo? info = _typeInfoResolver?.GetTypeInfo(type, this); + JsonTypeInfo? info = (_effectiveJsonTypeInfoResolver ?? _typeInfoResolver)?.GetTypeInfo(type, this); if (info != null) { 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 a91467a9943a3..05cd443bccf5a 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 @@ -437,9 +437,18 @@ public static void Options_JsonSerializerContext_DoesNotFallbackToReflection() } [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public static void Options_JsonSerializerContext_GetConverter_DoesNotFallBackToReflectionConverter() + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(false)] + [InlineData(true)] + public static void Options_JsonSerializerContext_GetConverter_DoesNotFallBackToReflectionConverter(bool isCompatibilitySwitchExplicitlyDisabled) { + var options = new RemoteInvokeOptions(); + + if (isCompatibilitySwitchExplicitlyDisabled) + { + options.RuntimeConfigurationOptions.Add("System.Text.Json.Serialization.EnableSourceGenReflectionFallback", false); + } + RemoteExecutor.Invoke(static () => { JsonContext context = JsonContext.Default; @@ -460,7 +469,40 @@ public static void Options_JsonSerializerContext_GetConverter_DoesNotFallBackToR Assert.Throws(() => context.Options.GetConverter(typeof(MyClass))); Assert.Throws(() => JsonSerializer.Serialize(unsupportedValue, context.Options)); - }).Dispose(); + }, options).Dispose(); + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void Options_JsonSerializerContext_Net6CompatibilitySwitch_FallsBackToReflectionResolver() + { + var options = new RemoteInvokeOptions + { + RuntimeConfigurationOptions = + { + ["System.Text.Json.Serialization.EnableSourceGenReflectionFallback"] = true + } + }; + + RemoteExecutor.Invoke(static () => + { + var unsupportedValue = new MyClass { Value = "value" }; + + // JsonSerializerContext does not return metadata for the type + Assert.Null(JsonContext.Default.GetTypeInfo(typeof(MyClass))); + + // Serialization fails using the JsonSerializerContext overload + Assert.Throws(() => JsonSerializer.Serialize(unsupportedValue, unsupportedValue.GetType(), JsonContext.Default)); + + // Serialization uses reflection fallback using the JsonSerializerOptions overload + string json = JsonSerializer.Serialize(unsupportedValue, JsonContext.Default.Options); + JsonTestHelper.AssertJsonEqual("""{"Value":"value", "Thing":null}""", json); + + // A converter can be resolved when looking up JsonSerializerOptions + JsonConverter converter = JsonContext.Default.Options.GetConverter(typeof(MyClass)); + Assert.IsAssignableFrom>(converter); + + }, options).Dispose(); } [Fact]