Skip to content

Commit f0eb5ce

Browse files
Implement an AppContext compatibility switch re-enabling reflection fallback in STJ source generators. (#75615) (#75694)
* Implement an AppContext compatibility switch re-enabling reflection fallback in sourcegen. * address feedback
1 parent 393e1ab commit f0eb5ce

File tree

4 files changed

+80
-5
lines changed

4 files changed

+80
-5
lines changed

src/libraries/System.Text.Json/src/System.Text.Json.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
3636
<Compile Include="..\Common\JsonSourceGenerationMode.cs" Link="Common\System\Text\Json\Serialization\JsonSourceGenerationMode.cs" />
3737
<Compile Include="..\Common\JsonSourceGenerationOptionsAttribute.cs" Link="Common\System\Text\Json\Serialization\JsonSourceGenerationOptionsAttribute.cs" />
3838
<Compile Include="..\Common\ReflectionExtensions.cs" Link="Common\System\Text\Json\Serialization\ReflectionExtensions.cs" />
39+
<Compile Include="System\Text\Json\AppContextSwitchHelper.cs" />
3940
<Compile Include="System\Text\Json\BitStack.cs" />
4041
<Compile Include="System\Text\Json\Document\JsonDocument.cs" />
4142
<Compile Include="System\Text\Json\Document\JsonDocument.DbRow.cs" />
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System.Text.Json
5+
{
6+
internal static class AppContextSwitchHelper
7+
{
8+
public static bool IsSourceGenReflectionFallbackEnabled => s_isSourceGenReflectionFallbackEnabled;
9+
10+
private static readonly bool s_isSourceGenReflectionFallbackEnabled =
11+
AppContext.TryGetSwitch(
12+
switchName: "System.Text.Json.Serialization.EnableSourceGenReflectionFallback",
13+
isEnabled: out bool value)
14+
? value : false;
15+
}
16+
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -628,14 +628,30 @@ internal void InitializeForReflectionSerializer()
628628
// Even if a resolver has already been specified, we need to root
629629
// the default resolver to gain access to the default converters.
630630
DefaultJsonTypeInfoResolver defaultResolver = DefaultJsonTypeInfoResolver.RootDefaultInstance();
631-
_typeInfoResolver ??= defaultResolver;
631+
632+
switch (_typeInfoResolver)
633+
{
634+
case null:
635+
// Use the default reflection-based resolver if no resolver has been specified.
636+
_typeInfoResolver = defaultResolver;
637+
break;
638+
639+
case JsonSerializerContext ctx when AppContextSwitchHelper.IsSourceGenReflectionFallbackEnabled:
640+
// .NET 6 compatibility mode: enable fallback to reflection metadata for JsonSerializerContext
641+
_effectiveJsonTypeInfoResolver = JsonTypeInfoResolver.Combine(ctx, defaultResolver);
642+
break;
643+
}
644+
632645
IsImmutable = true;
633646
_isInitializedForReflectionSerializer = true;
634647
}
635648

636649
internal bool IsInitializedForReflectionSerializer => _isInitializedForReflectionSerializer;
637650
private volatile bool _isInitializedForReflectionSerializer;
638651

652+
// Only populated in .NET 6 compatibility mode encoding reflection fallback in source gen
653+
private IJsonTypeInfoResolver? _effectiveJsonTypeInfoResolver;
654+
639655
internal void InitializeForMetadataGeneration()
640656
{
641657
if (_typeInfoResolver is null)
@@ -648,7 +664,7 @@ internal void InitializeForMetadataGeneration()
648664

649665
private JsonTypeInfo? GetTypeInfoNoCaching(Type type)
650666
{
651-
JsonTypeInfo? info = _typeInfoResolver?.GetTypeInfo(type, this);
667+
JsonTypeInfo? info = (_effectiveJsonTypeInfoResolver ?? _typeInfoResolver)?.GetTypeInfo(type, this);
652668

653669
if (info != null)
654670
{

src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,9 +437,18 @@ public static void Options_JsonSerializerContext_DoesNotFallbackToReflection()
437437
}
438438

439439
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
440-
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
441-
public static void Options_JsonSerializerContext_GetConverter_DoesNotFallBackToReflectionConverter()
440+
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
441+
[InlineData(false)]
442+
[InlineData(true)]
443+
public static void Options_JsonSerializerContext_GetConverter_DoesNotFallBackToReflectionConverter(bool isCompatibilitySwitchExplicitlyDisabled)
442444
{
445+
var options = new RemoteInvokeOptions();
446+
447+
if (isCompatibilitySwitchExplicitlyDisabled)
448+
{
449+
options.RuntimeConfigurationOptions.Add("System.Text.Json.Serialization.EnableSourceGenReflectionFallback", false);
450+
}
451+
443452
RemoteExecutor.Invoke(static () =>
444453
{
445454
JsonContext context = JsonContext.Default;
@@ -460,7 +469,40 @@ public static void Options_JsonSerializerContext_GetConverter_DoesNotFallBackToR
460469
Assert.Throws<NotSupportedException>(() => context.Options.GetConverter(typeof(MyClass)));
461470
Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(unsupportedValue, context.Options));
462471

463-
}).Dispose();
472+
}, options).Dispose();
473+
}
474+
475+
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
476+
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
477+
public static void Options_JsonSerializerContext_Net6CompatibilitySwitch_FallsBackToReflectionResolver()
478+
{
479+
var options = new RemoteInvokeOptions
480+
{
481+
RuntimeConfigurationOptions =
482+
{
483+
["System.Text.Json.Serialization.EnableSourceGenReflectionFallback"] = true
484+
}
485+
};
486+
487+
RemoteExecutor.Invoke(static () =>
488+
{
489+
var unsupportedValue = new MyClass { Value = "value" };
490+
491+
// JsonSerializerContext does not return metadata for the type
492+
Assert.Null(JsonContext.Default.GetTypeInfo(typeof(MyClass)));
493+
494+
// Serialization fails using the JsonSerializerContext overload
495+
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(unsupportedValue, unsupportedValue.GetType(), JsonContext.Default));
496+
497+
// Serialization uses reflection fallback using the JsonSerializerOptions overload
498+
string json = JsonSerializer.Serialize(unsupportedValue, JsonContext.Default.Options);
499+
JsonTestHelper.AssertJsonEqual("""{"Value":"value", "Thing":null}""", json);
500+
501+
// A converter can be resolved when looking up JsonSerializerOptions
502+
JsonConverter converter = JsonContext.Default.Options.GetConverter(typeof(MyClass));
503+
Assert.IsAssignableFrom<JsonConverter<MyClass>>(converter);
504+
505+
}, options).Dispose();
464506
}
465507

466508
[Fact]

0 commit comments

Comments
 (0)