Skip to content

Commit f0fe390

Browse files
Mark JsonSerializerOptions.TypeInfoResolver as nullable and linker-safe (#72044)
* Mark JsonSerializerOptions.TypeInfoResolver as nullable and linker-safe * Update src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs * Ensure TypeInfoResolver value is always populated in JsonSerializerOptions.Default
1 parent d434c4a commit f0fe390

17 files changed

+108
-96
lines changed

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults defaults) {
339339
public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { }
340340
public bool AllowTrailingCommas { get { throw null; } set { } }
341341
public System.Collections.Generic.IList<System.Text.Json.Serialization.JsonConverter> Converters { get { throw null; } }
342-
public static System.Text.Json.JsonSerializerOptions Default { get { throw null; } }
342+
public static System.Text.Json.JsonSerializerOptions Default { [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; } }
343343
public int DefaultBufferSize { get { throw null; } set { } }
344344
public System.Text.Json.Serialization.JsonIgnoreCondition DefaultIgnoreCondition { get { throw null; } set { } }
345345
public System.Text.Json.JsonNamingPolicy? DictionaryKeyPolicy { get { throw null; } set { } }
@@ -356,16 +356,13 @@ 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]
360-
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 { } }
359+
public System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver? TypeInfoResolver { get { throw null; } set { } }
361360
public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } }
362361
public bool WriteIndented { get { throw null; } set { } }
363362
public void AddContext<TContext>() where TContext : System.Text.Json.Serialization.JsonSerializerContext, new() { }
364363
[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("Getting a converter for a type may require reflection which depends on runtime code generation.")]
365364
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Getting a converter for a type may require reflection which depends on unreferenced code.")]
366365
public System.Text.Json.Serialization.JsonConverter GetConverter(System.Type typeToConvert) { throw null; }
367-
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Getting a metadata for a type may require reflection which depends on unreferenced code.")]
368-
[System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Getting a metadata for a type may require reflection which depends on runtime code generation.")]
369366
public System.Text.Json.Serialization.Metadata.JsonTypeInfo GetTypeInfo(System.Type type) { throw null; }
370367
}
371368
public enum JsonTokenType : byte

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@
585585
<value>Built-in type converters have not been initialized. There is no converter available for type '{0}'. To root all built-in converters, use a 'JsonSerializerOptions'-based method of the 'JsonSerializer'.</value>
586586
</data>
587587
<data name="NoMetadataForType" xml:space="preserve">
588-
<value>Metadata for type '{0}' was not provided to the serializer. The serializer method used does not support reflection-based creation of serialization-related type metadata. If using source generation, ensure that all root types passed to the serializer have been indicated with 'JsonSerializableAttribute', along with any types that might be serialized polymorphically.</value>
588+
<value>Metadata for type '{0}' was not provided by TypeInfoResolver of type '{1}'. If using source generation, ensure that all root types passed to the serializer have been indicated with 'JsonSerializableAttribute', along with any types that might be serialized polymorphically.</value>
589589
</data>
590590
<data name="CollectionIsReadOnly" xml:space="preserve">
591591
<value>Collection is read-only.</value>

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonArray.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio
165165
CreateNodes();
166166
Debug.Assert(_list != null);
167167

168-
options ??= JsonSerializerOptions.Default;
168+
options ??= s_defaultOptions;
169169

170170
writer.WriteStartArray();
171171

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.To.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ namespace System.Text.Json.Nodes
55
{
66
public abstract partial class JsonNode
77
{
8+
// linker-safe default JsonSerializerOptions instance used by JsonNode methods.
9+
private protected readonly JsonSerializerOptions s_defaultOptions = new();
10+
811
/// <summary>
912
/// Converts the current instance to string in JSON format.
1013
/// </summary>

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio
9696
}
9797
else
9898
{
99-
options ??= JsonSerializerOptions.Default;
99+
options ??= s_defaultOptions;
100100

101101
writer.WriteStartObject();
102102

src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueTrimmable.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio
3434

3535
if (_converter != null)
3636
{
37-
options ??= JsonSerializerOptions.Default;
37+
options ??= s_defaultOptions;
3838

3939
if (_converter.IsInternalConverterForNumberType)
4040
{

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

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

2222
options ??= JsonSerializerOptions.Default;
23-
options.InitializeForReflectionSerializer();
23+
24+
if (!options.IsLockedInstance || !DefaultJsonTypeInfoResolver.IsDefaultInstanceRooted)
25+
{
26+
options.InitializeForReflectionSerializer();
27+
}
2428

2529
return options.GetTypeInfoForRootType(runtimeType);
2630
}
@@ -33,7 +37,7 @@ private static JsonTypeInfo GetTypeInfo(JsonSerializerContext context, Type type
3337
JsonTypeInfo? info = context.GetTypeInfo(type);
3438
if (info is null)
3539
{
36-
ThrowHelper.ThrowInvalidOperationException_NoMetadataForType(type);
40+
ThrowHelper.ThrowInvalidOperationException_NoMetadataForType(type, context);
3741
}
3842

3943
return info;

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ public sealed partial class JsonSerializerOptions
3333
///
3434
/// If the <see cref="JsonSerializerOptions"/> instance is locked for modification, the method will return a cached instance for the metadata.
3535
/// </remarks>
36-
[RequiresUnreferencedCode("Getting a metadata for a type may require reflection which depends on unreferenced code.")]
37-
[RequiresDynamicCode("Getting a metadata for a type may require reflection which depends on runtime code generation.")]
3836
public JsonTypeInfo GetTypeInfo(Type type)
3937
{
4038
if (type is null)
@@ -47,8 +45,6 @@ public JsonTypeInfo GetTypeInfo(Type type)
4745
ThrowHelper.ThrowArgumentException_CannotSerializeInvalidType(nameof(type), type, null, null);
4846
}
4947

50-
_typeInfoResolver ??= DefaultJsonTypeInfoResolver.RootDefaultInstance();
51-
5248
JsonTypeInfo? typeInfo;
5349
if (IsLockedInstance)
5450
{
@@ -62,7 +58,7 @@ public JsonTypeInfo GetTypeInfo(Type type)
6258

6359
if (typeInfo is null)
6460
{
65-
ThrowHelper.ThrowNotSupportedException_NoMetadataForType(type);
61+
ThrowHelper.ThrowNotSupportedException_NoMetadataForType(type, TypeInfoResolver);
6662
}
6763

6864
return typeInfo;
@@ -82,7 +78,7 @@ internal JsonTypeInfo GetTypeInfoCached(Type type)
8278

8379
if (typeInfo == null)
8480
{
85-
ThrowHelper.ThrowNotSupportedException_NoMetadataForType(type);
81+
ThrowHelper.ThrowNotSupportedException_NoMetadataForType(type, TypeInfoResolver);
8682
return null;
8783
}
8884

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,13 @@ public JsonConverter GetConverter(Type typeToConvert)
4646
ThrowHelper.ThrowArgumentNullException(nameof(typeToConvert));
4747
}
4848

49-
_typeInfoResolver ??= DefaultJsonTypeInfoResolver.RootDefaultInstance();
49+
if (_typeInfoResolver is null)
50+
{
51+
// Backward compatibility -- root the default reflection converters
52+
// but do not populate the TypeInfoResolver setting.
53+
DefaultJsonTypeInfoResolver.RootDefaultInstance();
54+
}
55+
5056
return GetConverterFromTypeInfo(typeToConvert);
5157
}
5258

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

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Text.Json.Nodes;
1111
using System.Text.Json.Serialization;
1212
using System.Text.Json.Serialization.Metadata;
13+
using System.Threading;
1314

1415
namespace System.Text.Json
1516
{
@@ -32,7 +33,22 @@ public sealed partial class JsonSerializerOptions
3233
/// so using fresh default instances every time one is needed can result in redundant recomputation of converters.
3334
/// This property provides a shared instance that can be consumed by any number of components without necessitating any converter recomputation.
3435
/// </remarks>
35-
public static JsonSerializerOptions Default { get; } = CreateDefaultImmutableInstance();
36+
public static JsonSerializerOptions Default
37+
{
38+
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
39+
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
40+
get
41+
{
42+
if (s_defaultOptions is not JsonSerializerOptions options)
43+
{
44+
options = GetOrCreateDefaultOptionsInstance();
45+
}
46+
47+
return options;
48+
}
49+
}
50+
51+
private static JsonSerializerOptions? s_defaultOptions;
3652

3753
// For any new option added, adding it to the options copied in the copy constructor below must be considered.
3854
private IJsonTypeInfoResolver? _typeInfoResolver;
@@ -162,16 +178,15 @@ public JsonSerializerOptions(JsonSerializerDefaults defaults) : this()
162178
/// Gets or sets a <see cref="JsonTypeInfo"/> contract resolver.
163179
/// </summary>
164180
/// <remarks>
165-
/// A <see langword="null"/> setting is equivalent to using the reflection-based <see cref="DefaultJsonTypeInfoResolver"/>.
181+
/// When used with the reflection-based <see cref="JsonSerializer"/> APIs,
182+
/// a <see langword="null"/> setting be equivalent to and replaced by the reflection-based
183+
/// <see cref="DefaultJsonTypeInfoResolver"/>.
166184
/// </remarks>
167-
[AllowNull]
168-
public IJsonTypeInfoResolver TypeInfoResolver
185+
public IJsonTypeInfoResolver? TypeInfoResolver
169186
{
170-
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
171-
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
172187
get
173188
{
174-
return _typeInfoResolver ??= DefaultJsonTypeInfoResolver.RootDefaultInstance();
189+
return _typeInfoResolver;
175190
}
176191
set
177192
{
@@ -180,9 +195,6 @@ public IJsonTypeInfoResolver TypeInfoResolver
180195
}
181196
}
182197

183-
// Needed since public property is RequiresUnreferencedCode.
184-
internal IJsonTypeInfoResolver? TypeInfoResolverSafe => _typeInfoResolver;
185-
186198
/// <summary>
187199
/// Defines whether an extra comma at the end of a list of JSON values in an object or array
188200
/// is allowed (and ignored) within the JSON payload being deserialized.
@@ -598,6 +610,7 @@ internal bool IsLockedInstance
598610
set
599611
{
600612
Debug.Assert(value, "cannot unlock options instances");
613+
Debug.Assert(_typeInfoResolver != null, "cannot lock without a resolver.");
601614
_isLockedInstance = true;
602615
}
603616
}
@@ -609,43 +622,22 @@ internal bool IsLockedInstance
609622
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
610623
internal void InitializeForReflectionSerializer()
611624
{
612-
if (!_isInitializedForReflectionSerializer)
613-
{
614-
DefaultJsonTypeInfoResolver defaultResolver = DefaultJsonTypeInfoResolver.RootDefaultInstance();
615-
_typeInfoResolver ??= defaultResolver;
616-
IsLockedInstance = true;
617-
618-
CachingContext? context = GetCachingContext();
619-
Debug.Assert(context != null);
620-
621-
if (context.Options != this)
622-
{
623-
// We're using a shared caching context deriving from a different options instance;
624-
// for coherence ensure that it has been opted in for reflection-based serialization as well.
625-
context.Options.InitializeForReflectionSerializer();
626-
}
627-
628-
_isInitializedForReflectionSerializer = true;
629-
}
625+
// Even if a resolver has already been specified, we need to root
626+
// the default resolver to gain access to the default converters.
627+
DefaultJsonTypeInfoResolver defaultResolver = DefaultJsonTypeInfoResolver.RootDefaultInstance();
628+
_typeInfoResolver ??= defaultResolver;
629+
IsLockedInstance = true;
630630
}
631631

632-
private volatile bool _isInitializedForReflectionSerializer;
633-
634632
internal void InitializeForMetadataGeneration()
635633
{
636-
if (!_isInitializedForMetadataGeneration)
634+
if (_typeInfoResolver is null)
637635
{
638-
if (_typeInfoResolver is null)
639-
{
640-
ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoUsedButTypeInfoResolverNotSet();
641-
}
642-
643-
IsLockedInstance = true;
644-
_isInitializedForMetadataGeneration = true;
636+
ThrowHelper.ThrowInvalidOperationException_JsonTypeInfoUsedButTypeInfoResolverNotSet();
645637
}
646-
}
647638

648-
private volatile bool _isInitializedForMetadataGeneration;
639+
IsLockedInstance = true;
640+
}
649641

650642
private JsonTypeInfo? GetTypeInfoNoCaching(Type type)
651643
{
@@ -730,10 +722,17 @@ public ConverterList(JsonSerializerOptions options, IList<JsonConverter>? source
730722
protected override void VerifyMutable() => _options.VerifyMutable();
731723
}
732724

733-
private static JsonSerializerOptions CreateDefaultImmutableInstance()
725+
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
726+
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
727+
private static JsonSerializerOptions GetOrCreateDefaultOptionsInstance()
734728
{
735-
var options = new JsonSerializerOptions { IsLockedInstance = true };
736-
return options;
729+
var options = new JsonSerializerOptions
730+
{
731+
TypeInfoResolver = DefaultJsonTypeInfoResolver.RootDefaultInstance(),
732+
IsLockedInstance = true
733+
};
734+
735+
return Interlocked.CompareExchange(ref s_defaultOptions, options, null) ?? options;
737736
}
738737
}
739738
}

0 commit comments

Comments
 (0)