Skip to content

Commit 6b2349a

Browse files
Implement an AppContext compatibility switch re-enabling reflection fallback in STJ source generators. (#75615)
* Implement an AppContext compatibility switch re-enabling reflection fallback in sourcegen. * address feedback
1 parent 403b05c commit 6b2349a

4 files changed

Lines changed: 80 additions & 5 deletions

File tree

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
@@ -643,17 +643,33 @@ internal void InitializeForReflectionSerializer()
643643
// Even if a resolver has already been specified, we need to root
644644
// the default resolver to gain access to the default converters.
645645
DefaultJsonTypeInfoResolver defaultResolver = DefaultJsonTypeInfoResolver.RootDefaultInstance();
646-
_typeInfoResolver ??= defaultResolver;
646+
647+
switch (_typeInfoResolver)
648+
{
649+
case null:
650+
// Use the default reflection-based resolver if no resolver has been specified.
651+
_typeInfoResolver = defaultResolver;
652+
break;
653+
654+
case JsonSerializerContext ctx when AppContextSwitchHelper.IsSourceGenReflectionFallbackEnabled:
655+
// .NET 6 compatibility mode: enable fallback to reflection metadata for JsonSerializerContext
656+
_effectiveJsonTypeInfoResolver = JsonTypeInfoResolver.Combine(ctx, defaultResolver);
657+
break;
658+
}
659+
647660
MakeReadOnly();
648661
_isInitializedForReflectionSerializer = true;
649662
}
650663

651664
internal bool IsInitializedForReflectionSerializer => _isInitializedForReflectionSerializer;
652665
private volatile bool _isInitializedForReflectionSerializer;
653666

667+
// Only populated in .NET 6 compatibility mode encoding reflection fallback in source gen
668+
private IJsonTypeInfoResolver? _effectiveJsonTypeInfoResolver;
669+
654670
private JsonTypeInfo? GetTypeInfoNoCaching(Type type)
655671
{
656-
JsonTypeInfo? info = _typeInfoResolver?.GetTypeInfo(type, this);
672+
JsonTypeInfo? info = (_effectiveJsonTypeInfoResolver ?? _typeInfoResolver)?.GetTypeInfo(type, this);
657673

658674
if (info != null)
659675
{

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
@@ -475,9 +475,18 @@ public static void Options_JsonSerializerContext_DoesNotFallbackToReflection()
475475
}
476476

477477
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
478-
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
479-
public static void Options_JsonSerializerContext_GetConverter_DoesNotFallBackToReflectionConverter()
478+
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
479+
[InlineData(false)]
480+
[InlineData(true)]
481+
public static void Options_JsonSerializerContext_GetConverter_DoesNotFallBackToReflectionConverter(bool isCompatibilitySwitchExplicitlyDisabled)
480482
{
483+
var options = new RemoteInvokeOptions();
484+
485+
if (isCompatibilitySwitchExplicitlyDisabled)
486+
{
487+
options.RuntimeConfigurationOptions.Add("System.Text.Json.Serialization.EnableSourceGenReflectionFallback", false);
488+
}
489+
481490
RemoteExecutor.Invoke(static () =>
482491
{
483492
JsonContext context = JsonContext.Default;
@@ -498,7 +507,40 @@ public static void Options_JsonSerializerContext_GetConverter_DoesNotFallBackToR
498507
Assert.Throws<NotSupportedException>(() => context.Options.GetConverter(typeof(MyClass)));
499508
Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(unsupportedValue, context.Options));
500509

501-
}).Dispose();
510+
}, options).Dispose();
511+
}
512+
513+
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
514+
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
515+
public static void Options_JsonSerializerContext_Net6CompatibilitySwitch_FallsBackToReflectionResolver()
516+
{
517+
var options = new RemoteInvokeOptions
518+
{
519+
RuntimeConfigurationOptions =
520+
{
521+
["System.Text.Json.Serialization.EnableSourceGenReflectionFallback"] = true
522+
}
523+
};
524+
525+
RemoteExecutor.Invoke(static () =>
526+
{
527+
var unsupportedValue = new MyClass { Value = "value" };
528+
529+
// JsonSerializerContext does not return metadata for the type
530+
Assert.Null(JsonContext.Default.GetTypeInfo(typeof(MyClass)));
531+
532+
// Serialization fails using the JsonSerializerContext overload
533+
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(unsupportedValue, unsupportedValue.GetType(), JsonContext.Default));
534+
535+
// Serialization uses reflection fallback using the JsonSerializerOptions overload
536+
string json = JsonSerializer.Serialize(unsupportedValue, JsonContext.Default.Options);
537+
JsonTestHelper.AssertJsonEqual("""{"Value":"value", "Thing":null}""", json);
538+
539+
// A converter can be resolved when looking up JsonSerializerOptions
540+
JsonConverter converter = JsonContext.Default.Options.GetConverter(typeof(MyClass));
541+
Assert.IsAssignableFrom<JsonConverter<MyClass>>(converter);
542+
543+
}, options).Dispose();
502544
}
503545

504546
[Fact]

0 commit comments

Comments
 (0)