Skip to content

Commit 8e0a132

Browse files
eiriktsarpalisstephentoubkrwq
authored
Remove implicit fallback to reflection-based serialization (#71746)
* Remove implicit fallback to reflection-based serialization. Fix #71714 Include JsonSerializerContext in JsonSerializerOptions copy constructor. Fix #71716 Move reflection-based converter resolution out of JsonSerializerOptions. Fix #68878 * Address feedback & add one more test * Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs Co-authored-by: Stephen Toub <stoub@microsoft.com> * fix build * Bring back throwing behavior in JsonSerializerContext and add tests * Only create caching contexts if a resolver is populated * Add null test for JsonSerializerContext interface implementation. * skip RemoteExecutor test in netfx targets * Add DefaultJsonTypeInfoResolver test for types with JsonConverterAttribute * remove nullability annotation * Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs Co-authored-by: Krzysztof Wicher <mordotymoja@gmail.com> Co-authored-by: Stephen Toub <stoub@microsoft.com> Co-authored-by: Krzysztof Wicher <mordotymoja@gmail.com>
1 parent 527f1d1 commit 8e0a132

27 files changed

Lines changed: 400 additions & 340 deletions

src/libraries/System.Text.Json/ref/System.Text.Json.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { }
356356
public System.Text.Json.JsonNamingPolicy? PropertyNamingPolicy { get { throw null; } set { } }
357357
public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } }
358358
public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get { throw null; } set { } }
359+
[System.Diagnostics.CodeAnalysis.AllowNullAttribute]
359360
public System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver TypeInfoResolver { [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.")] get { throw null; } set { } }
360361
public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } }
361362
public bool WriteIndented { get { throw null; } set { } }

src/libraries/System.Text.Json/src/Resources/Strings.resx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,10 +400,10 @@
400400
<value>The converter '{0}' is not compatible with the type '{1}'.</value>
401401
</data>
402402
<data name="ResolverTypeNotCompatible" xml:space="preserve">
403-
<value>TypeInfoResolver expected to return JsonTypeInfo of type '{0}' but returned JsonTypeInfo of type '{1}'.</value>
403+
<value>The IJsonTypeInfoResolver returned an incompatible JsonTypeInfo instance of type '{0}', expected type '{1}'.</value>
404404
</data>
405405
<data name="ResolverTypeInfoOptionsNotCompatible" xml:space="preserve">
406-
<value>TypeInfoResolver expected to return JsonTypeInfo options bound to the JsonSerializerOptions provided in the argument.</value>
406+
<value>The IJsonTypeInfoResolver returned a JsonTypeInfo instance whose JsonSerializerOptions setting does not match the provided argument.</value>
407407
</data>
408408
<data name="SerializationConverterWrite" xml:space="preserve">
409409
<value>The converter '{0}' wrote too much or not enough.</value>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ internal JsonConverter GetConverterInternal(Type typeToConvert, JsonSerializerOp
6565
break;
6666
}
6767

68-
return converter!;
68+
return converter;
6969
}
7070

7171
internal sealed override object ReadCoreAsObject(

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ internal sealed override JsonParameterInfo CreateJsonParameterInfo()
8484

8585
internal sealed override JsonConverter<TTarget> CreateCastingConverter<TTarget>()
8686
{
87+
JsonSerializerOptions.CheckConverterNullabilityIsSameAsPropertyType(this, typeof(TTarget));
8788
return new CastingConverter<TTarget, T>(this);
8889
}
8990

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,9 @@ private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type run
2020
Debug.Assert(runtimeType != null);
2121

2222
options ??= JsonSerializerOptions.Default;
23-
if (!options.IsInitializedForReflectionSerializer)
24-
{
25-
options.InitializeForReflectionSerializer();
26-
}
23+
options.InitializeForReflectionSerializer();
2724

28-
return options.GetOrAddJsonTypeInfoForRootType(runtimeType);
25+
return options.GetJsonTypeInfoForRootType(runtimeType);
2926
}
3027

3128
private static JsonTypeInfo GetTypeInfo(JsonSerializerContext context, Type type)

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -366,13 +366,7 @@ public static partial class JsonSerializer
366366
ThrowHelper.ThrowArgumentNullException(nameof(utf8Json));
367367
}
368368

369-
options ??= JsonSerializerOptions.Default;
370-
if (!options.IsInitializedForReflectionSerializer)
371-
{
372-
options.InitializeForReflectionSerializer();
373-
}
374-
375-
JsonTypeInfo jsonTypeInfo = options.GetOrAddJsonTypeInfoForRootType(typeof(TValue));
369+
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, typeof(TValue));
376370
return CreateAsyncEnumerableDeserializer(utf8Json, CreateQueueTypeInfo<TValue>(jsonTypeInfo), cancellationToken);
377371
}
378372

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,7 @@ private static void WriteUsingSerializer<TValue>(Utf8JsonWriter writer, in TValu
5757
{
5858
Debug.Assert(writer != null);
5959

60-
Debug.Assert(!jsonTypeInfo.HasSerialize ||
61-
jsonTypeInfo is not JsonTypeInfo<TValue> ||
62-
jsonTypeInfo.Options.SerializerContext == null ||
63-
!jsonTypeInfo.Options.SerializerContext.CanUseSerializationLogic,
64-
"Incorrect method called. WriteUsingGeneratedSerializer() should have been called instead.");
60+
// TODO unify method with WriteUsingGeneratedSerializer
6561

6662
WriteStack state = default;
6763
jsonTypeInfo.EnsureConfigured();

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,27 @@ public abstract partial class JsonSerializerContext : IJsonTypeInfoResolver
1313
{
1414
private bool? _canUseSerializationLogic;
1515

16-
internal JsonSerializerOptions? _options;
16+
private JsonSerializerOptions? _options;
1717

1818
/// <summary>
1919
/// Gets the run time specified options of the context. If no options were passed
2020
/// when instanciating the context, then a new instance is bound and returned.
2121
/// </summary>
2222
/// <remarks>
23-
/// The instance cannot be mutated once it is bound to the context instance.
23+
/// The options instance cannot be mutated once it is bound to the context instance.
2424
/// </remarks>
25-
public JsonSerializerOptions Options => _options ??= new JsonSerializerOptions { TypeInfoResolver = this };
25+
public JsonSerializerOptions Options
26+
{
27+
get => _options ??= new JsonSerializerOptions { TypeInfoResolver = this, IsLockedInstance = true };
28+
29+
internal set
30+
{
31+
Debug.Assert(!value.IsLockedInstance);
32+
value.TypeInfoResolver = this;
33+
value.IsLockedInstance = true;
34+
_options = value;
35+
}
36+
}
2637

2738
/// <summary>
2839
/// Indicates whether pre-generated serialization logic for types in the context
@@ -84,8 +95,8 @@ protected JsonSerializerContext(JsonSerializerOptions? options)
8495
{
8596
if (options != null)
8697
{
87-
options.TypeInfoResolver = this;
88-
Debug.Assert(_options == options, "options.TypeInfoResolver setter did not assign options");
98+
options.VerifyMutable();
99+
Options = options;
89100
}
90101
}
91102

@@ -98,10 +109,9 @@ protected JsonSerializerContext(JsonSerializerOptions? options)
98109

99110
JsonTypeInfo? IJsonTypeInfoResolver.GetTypeInfo(Type type, JsonSerializerOptions options)
100111
{
101-
if (options != null && _options != options)
112+
if (options != null && options != _options)
102113
{
103-
// TODO is this the appropriate exception message to throw?
104-
ThrowHelper.ThrowInvalidOperationException_SerializerContextOptionsImmutable();
114+
ThrowHelper.ThrowInvalidOperationException_ResolverTypeInfoOptionsNotCompatible();
105115
}
106116

107117
return GetTypeInfo(type);

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

Lines changed: 26 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -26,27 +26,26 @@ public sealed partial class JsonSerializerOptions
2626
/// <summary>
2727
/// This method returns configured non-null JsonTypeInfo
2828
/// </summary>
29-
internal JsonTypeInfo GetOrAddJsonTypeInfo(Type type)
29+
internal JsonTypeInfo GetJsonTypeInfoCached(Type type)
3030
{
31-
if (_cachingContext == null)
31+
JsonTypeInfo? typeInfo = null;
32+
33+
if (IsLockedInstance)
3234
{
33-
InitializeCachingContext();
35+
typeInfo = GetCachingContext()?.GetOrAddJsonTypeInfo(type);
3436
}
3537

36-
JsonTypeInfo? typeInfo = _cachingContext.GetOrAddJsonTypeInfo(type);
37-
3838
if (typeInfo == null)
3939
{
4040
ThrowHelper.ThrowNotSupportedException_NoMetadataForType(type);
4141
return null;
4242
}
4343

4444
typeInfo.EnsureConfigured();
45-
4645
return typeInfo;
4746
}
4847

49-
internal bool TryGetJsonTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo)
48+
internal bool TryGetJsonTypeInfoCached(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo)
5049
{
5150
if (_cachingContext == null)
5251
{
@@ -57,20 +56,18 @@ internal bool TryGetJsonTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo
5756
return _cachingContext.TryGetJsonTypeInfo(type, out typeInfo);
5857
}
5958

60-
internal bool IsJsonTypeInfoCached(Type type) => _cachingContext?.IsJsonTypeInfoCached(type) == true;
61-
6259
/// <summary>
6360
/// Return the TypeInfo for root API calls.
6461
/// This has an LRU cache that is intended only for public API calls that specify the root type.
6562
/// </summary>
6663
[MethodImpl(MethodImplOptions.AggressiveInlining)]
67-
internal JsonTypeInfo GetOrAddJsonTypeInfoForRootType(Type type)
64+
internal JsonTypeInfo GetJsonTypeInfoForRootType(Type type)
6865
{
6966
JsonTypeInfo? jsonTypeInfo = _lastTypeInfo;
7067

7168
if (jsonTypeInfo?.Type != type)
7269
{
73-
jsonTypeInfo = GetOrAddJsonTypeInfo(type);
70+
jsonTypeInfo = GetJsonTypeInfoCached(type);
7471
_lastTypeInfo = jsonTypeInfo;
7572
}
7673

@@ -83,11 +80,16 @@ internal void ClearCaches()
8380
_lastTypeInfo = null;
8481
}
8582

86-
[MemberNotNull(nameof(_cachingContext))]
87-
private void InitializeCachingContext()
83+
private CachingContext? GetCachingContext()
8884
{
89-
_isLockedInstance = true;
90-
_cachingContext = TrackedCachingContexts.GetOrCreate(this);
85+
Debug.Assert(IsLockedInstance);
86+
87+
if (_cachingContext is null && _typeInfoResolver is not null)
88+
{
89+
_cachingContext = TrackedCachingContexts.GetOrCreate(this);
90+
}
91+
92+
return _cachingContext;
9193
}
9294

9395
/// <summary>
@@ -98,7 +100,7 @@ private void InitializeCachingContext()
98100
/// </summary>
99101
internal sealed class CachingContext
100102
{
101-
private readonly ConcurrentDictionary<Type, JsonTypeInfo> _jsonTypeInfoCache = new();
103+
private readonly ConcurrentDictionary<Type, JsonTypeInfo?> _jsonTypeInfoCache = new();
102104

103105
public CachingContext(JsonSerializerOptions options)
104106
{
@@ -110,24 +112,8 @@ public CachingContext(JsonSerializerOptions options)
110112
// If changing please ensure that src/ILLink.Descriptors.LibraryBuild.xml is up-to-date.
111113
public int Count => _jsonTypeInfoCache.Count;
112114

113-
public JsonTypeInfo? GetOrAddJsonTypeInfo(Type type)
114-
{
115-
if (_jsonTypeInfoCache.TryGetValue(type, out JsonTypeInfo? typeInfo))
116-
{
117-
return typeInfo;
118-
}
119-
120-
typeInfo = Options.GetTypeInfoInternal(type);
121-
if (typeInfo != null)
122-
{
123-
return _jsonTypeInfoCache.GetOrAdd(type, _ => typeInfo);
124-
}
125-
126-
return null;
127-
}
128-
115+
public JsonTypeInfo? GetOrAddJsonTypeInfo(Type type) => _jsonTypeInfoCache.GetOrAdd(type, Options.GetTypeInfoNoCaching);
129116
public bool TryGetJsonTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo) => _jsonTypeInfoCache.TryGetValue(type, out typeInfo);
130-
public bool IsJsonTypeInfoCached(Type type) => _jsonTypeInfoCache.ContainsKey(type);
131117

132118
public void Clear()
133119
{
@@ -147,12 +133,14 @@ internal static class TrackedCachingContexts
147133
new(concurrencyLevel: 1, capacity: MaxTrackedContexts, new EqualityComparer());
148134

149135
private const int EvictionCountHistory = 16;
150-
private static Queue<int> s_recentEvictionCounts = new(EvictionCountHistory);
136+
private static readonly Queue<int> s_recentEvictionCounts = new(EvictionCountHistory);
151137
private static int s_evictionRunsToSkip;
152138

153139
public static CachingContext GetOrCreate(JsonSerializerOptions options)
154140
{
155-
Debug.Assert(options._isLockedInstance, "Cannot create caching contexts for mutable JsonSerializerOptions instances");
141+
Debug.Assert(options.IsLockedInstance, "Cannot create caching contexts for mutable JsonSerializerOptions instances");
142+
Debug.Assert(options._typeInfoResolver != null);
143+
156144
ConcurrentDictionary<JsonSerializerOptions, WeakReference<CachingContext>> cache = s_cache;
157145

158146
if (cache.TryGetValue(options, out WeakReference<CachingContext>? wr) && wr.TryGetTarget(out CachingContext? ctx))
@@ -187,12 +175,7 @@ public static CachingContext GetOrCreate(JsonSerializerOptions options)
187175

188176
// Use a defensive copy of the options instance as key to
189177
// avoid capturing references to any caching contexts.
190-
var key = new JsonSerializerOptions(options)
191-
{
192-
// Copy fields ignored by the copy constructor
193-
// but are necessary to determine equivalence.
194-
_typeInfoResolver = options._typeInfoResolver,
195-
};
178+
var key = new JsonSerializerOptions(options);
196179
Debug.Assert(key._cachingContext == null);
197180

198181
ctx = new CachingContext(options);
@@ -312,7 +295,7 @@ public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right)
312295
left._includeFields == right._includeFields &&
313296
left._propertyNameCaseInsensitive == right._propertyNameCaseInsensitive &&
314297
left._writeIndented == right._writeIndented &&
315-
NormalizeResolver(left._typeInfoResolver) == NormalizeResolver(right._typeInfoResolver) &&
298+
left._typeInfoResolver == right._typeInfoResolver &&
316299
CompareLists(left._converters, right._converters);
317300

318301
static bool CompareLists<TValue>(ConfigurationList<TValue> left, ConfigurationList<TValue> right)
@@ -356,7 +339,7 @@ public int GetHashCode(JsonSerializerOptions options)
356339
hc.Add(options._includeFields);
357340
hc.Add(options._propertyNameCaseInsensitive);
358341
hc.Add(options._writeIndented);
359-
hc.Add(NormalizeResolver(options._typeInfoResolver));
342+
hc.Add(options._typeInfoResolver);
360343
GetHashCode(ref hc, options._converters);
361344

362345
static void GetHashCode<TValue>(ref HashCode hc, ConfigurationList<TValue> list)
@@ -370,10 +353,6 @@ static void GetHashCode<TValue>(ref HashCode hc, ConfigurationList<TValue> list)
370353
return hc.ToHashCode();
371354
}
372355

373-
// An options instance might be locked but not initialized for reflection serialization yet.
374-
private static IJsonTypeInfoResolver? NormalizeResolver(IJsonTypeInfoResolver? resolver)
375-
=> resolver ?? DefaultJsonTypeInfoResolver.DefaultInstance;
376-
377356
#if !NETCOREAPP
378357
/// <summary>
379358
/// Polyfill for System.HashCode.

0 commit comments

Comments
 (0)