diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index b1e111bbd937a4..ef6f5f7ce84f46 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -274,6 +274,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { } public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } } public bool WriteIndented { get { throw null; } set { } } public void AddContext() where TContext : System.Text.Json.Serialization.JsonSerializerContext, new() { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Getting a converter for a type may require reflection which depends on unreferenced code.")] public System.Text.Json.Serialization.JsonConverter GetConverter(System.Type typeToConvert) { throw null; } } public enum JsonTokenType : byte diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs index c9e659f3d79eb7..aec86617a81736 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverterFactory.cs @@ -20,7 +20,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer Type valueTypeToConvert = typeToConvert.GetGenericArguments()[0]; - JsonConverter valueConverter = options.GetConverter(valueTypeToConvert); + JsonConverter valueConverter = options.GetConverterInternal(valueTypeToConvert); Debug.Assert(valueConverter != null); // If the value type has an interface or object converter, just return that converter directly. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs index bbb73f3f3f37d2..597dce6b7ef175 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs @@ -39,7 +39,7 @@ internal override void WriteWithQuotes(Utf8JsonWriter writer, object? value, Jso Debug.Assert(value != null); Type runtimeType = value.GetType(); - JsonConverter runtimeConverter = options.GetConverter(runtimeType); + JsonConverter runtimeConverter = options.GetConverterInternal(runtimeType); if (runtimeConverter == this) { ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(runtimeType, this); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Span.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Span.cs index 4fbdb1aa306421..fd243fa0b81bf2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Span.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Span.cs @@ -124,9 +124,8 @@ public static partial class JsonSerializer [RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)] private static TValue? ReadUsingOptions(ReadOnlySpan utf8Json, Type returnType, JsonSerializerOptions? options) { - options ??= JsonSerializerOptions.s_defaultOptions; - options.RootBuiltInConvertersAndTypeInfoCreator(); - return ReadUsingMetadata(utf8Json, GetTypeInfo(returnType, options)); + JsonTypeInfo jsonTypeInfo = GetTypeInfo(returnType, options); + return ReadUsingMetadata(utf8Json, jsonTypeInfo); } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index 200ad85724a609..95de3062e667f8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -27,7 +27,7 @@ public sealed partial class JsonSerializerOptions private readonly ConcurrentDictionary _converters = new ConcurrentDictionary(); [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] - internal void RootBuiltInConvertersAndTypeInfoCreator() + private void RootBuiltInConverters() { s_defaultSimpleConverters ??= GetDefaultSimpleConverters(); s_defaultFactoryConverters ??= new JsonConverter[] @@ -45,11 +45,6 @@ internal void RootBuiltInConvertersAndTypeInfoCreator() // Object should always be last since it converts any type. new ObjectConverterFactory() }; - - _typeInfoCreationFunc ??= CreateJsonTypeInfo; - - [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] - static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options) => new JsonTypeInfo(type, options); } private static Dictionary GetDefaultSimpleConverters() @@ -119,7 +114,7 @@ internal JsonConverter DetermineConverter(Type? parentClassType, Type runtimePro if (converter == null) { - converter = GetConverter(runtimePropertyType); + converter = GetConverterInternal(runtimePropertyType); Debug.Assert(converter != null); } @@ -160,8 +155,22 @@ internal JsonConverter DetermineConverter(Type? parentClassType, Type runtimePro /// There is no compatible /// for or its serializable members. /// + [RequiresUnreferencedCode("Getting a converter for a type may require reflection which depends on unreferenced code.")] public JsonConverter GetConverter(Type typeToConvert) { + if (typeToConvert == null) + { + throw new ArgumentNullException(nameof(typeToConvert)); + } + + RootBuiltInConverters(); + return GetConverterInternal(typeToConvert); + } + + internal JsonConverter GetConverterInternal(Type typeToConvert) + { + Debug.Assert(typeToConvert != null); + if (_converters.TryGetValue(typeToConvert, out JsonConverter? converter)) { Debug.Assert(converter != null); 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 a7c5e64923f855..6d9bc7d910772c 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 @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text.Encodings.Web; using System.Text.Json.Node; @@ -569,6 +570,16 @@ internal MemberAccessor MemberAccessorStrategy } } + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + internal void RootBuiltInConvertersAndTypeInfoCreator() + { + RootBuiltInConverters(); + _typeInfoCreationFunc ??= CreateJsonTypeInfo; + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options) => new JsonTypeInfo(type, options); + } + internal JsonTypeInfo GetOrAddClass(Type type) { _haveTypesBeenCreated = true; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index e1f097411d8559..521b14f602ed86 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -435,7 +435,7 @@ internal bool ReadJsonExtensionDataValue(ref ReadStack state, ref Utf8JsonReader return true; } - JsonConverter converter = (JsonConverter)Options.GetConverter(typeof(JsonElement)); + JsonConverter converter = (JsonConverter)Options.GetConverterInternal(typeof(JsonElement)); if (!converter.TryRead(ref reader, typeof(JsonElement), Options, ref state, out JsonElement jsonElement)) { // JsonElement is a struct that must be read in full. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index 2f7e7614430aba..1f49a7f4ac2150 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -517,7 +517,7 @@ private bool DetermineExtensionDataProperty(Dictionary // Avoid a reference to typeof(JsonNode) to support trimming. (declaredPropertyType.FullName == JsonObjectTypeName && ReferenceEquals(declaredPropertyType.Assembly, GetType().Assembly))) { - converter = Options.GetConverter(declaredPropertyType); + converter = Options.GetConverterInternal(declaredPropertyType); Debug.Assert(converter != null); } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGeneratorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGeneratorTests.cs index 2d9f4db778e056..cc14bdcde626cc 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGeneratorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGeneratorTests.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using System.Text.Json.SourceGeneration.Tests; using System.Text.Json.SourceGeneration.Tests.JsonSourceGeneration; using Xunit; @@ -466,5 +468,84 @@ public class ClassWithEnumAndNullable public DayOfWeek Day { get; set; } public DayOfWeek? NullableDay { get; set; } } + + [Fact] + public static void Converters_AndTypeInfoCreator_NotRooted_WhenMetadataNotPresent() + { + object[] objArr = new object[] { new MyStruct() }; + + // Metadata not generated for MyStruct without JsonSerializableAttribute. + NotSupportedException ex = Assert.Throws( + () => JsonSerializer.Serialize(objArr, JsonContext.Default.ObjectArray)); + string exAsStr = ex.ToString(); + Assert.Contains(typeof(MyStruct).ToString(), exAsStr); + Assert.Contains("JsonSerializerOptions", exAsStr); + + // This test uses reflection to: + // - Access JsonSerializerOptions.s_defaultSimpleConverters + // - Access JsonSerializerOptions.s_defaultFactoryConverters + // - Access JsonSerializerOptions._typeInfoCreationFunc + // + // If any of them changes, this test will need to be kept in sync. + + // Confirm built-in converters not set. + AssertFieldNull("s_defaultSimpleConverters", optionsInstance: null); + AssertFieldNull("s_defaultFactoryConverters", optionsInstance: null); + + // Confirm type info dynamic creator not set. + AssertFieldNull("_typeInfoCreationFunc", JsonContext.Default.Options); + + static void AssertFieldNull(string fieldName, JsonSerializerOptions? optionsInstance) + { + BindingFlags bindingFlags = BindingFlags.NonPublic | (optionsInstance == null ? BindingFlags.Static : BindingFlags.Instance); + FieldInfo fieldInfo = typeof(JsonSerializerOptions).GetField(fieldName, bindingFlags); + Assert.NotNull(fieldInfo); + Assert.Null(fieldInfo.GetValue(optionsInstance)); + } + } + + private const string ExceptionMessageFromCustomContext = "Exception thrown from custom context."; + + [Fact] + public static void GetTypeInfoCalledDuringPolymorphicSerialization() + { + CustomContext context = new(new JsonSerializerOptions()); + + // Empty array is fine since we don't need metadata for children. + Assert.Equal("[]", JsonSerializer.Serialize(Array.Empty(), context.ObjectArray)); + Assert.Equal("[]", JsonSerializer.Serialize(Array.Empty(), typeof(object[]), context)); + + // GetTypeInfo method called to get metadata for element run-time type. + object[] objArr = new object[] { new MyStruct() }; + + InvalidOperationException ex = Assert.Throws(() => JsonSerializer.Serialize(objArr, context.ObjectArray)); + Assert.Contains(ExceptionMessageFromCustomContext, ex.ToString()); + + ex = Assert.Throws(() => JsonSerializer.Serialize(objArr, typeof(object[]), context)); + Assert.Contains(ExceptionMessageFromCustomContext, ex.ToString()); + } + + internal struct MyStruct { } + + internal class CustomContext : JsonSerializerContext + { + public CustomContext(JsonSerializerOptions options) : base(options) { } + + private JsonTypeInfo _object; + public JsonTypeInfo Object => _object ??= JsonMetadataServices.CreateValueInfo(Options, JsonMetadataServices.ObjectConverter); + + private JsonTypeInfo _objectArray; + public JsonTypeInfo ObjectArray => _objectArray ??= JsonMetadataServices.CreateArrayInfo(Options, Object, default); + + public override JsonTypeInfo GetTypeInfo(Type type) + { + if (type == typeof(object[])) + { + return ObjectArray; + } + + throw new InvalidOperationException(ExceptionMessageFromCustomContext); + } + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.cs index beb16ceeeac49d..80067292399c46 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CustomConverterTests/CustomConverterTests.cs @@ -160,5 +160,26 @@ public static void VerifyObjectConverterWithPreservedReferences() Assert.IsType(obj); Assert.Equal(true, obj); } + + [Fact] + public static void GetConverterRootsBuiltInConverters() + { + JsonSerializerOptions options = new(); + RunTest(); + RunTest(); + + void RunTest() + { + JsonConverter converter = options.GetConverter(typeof(TConverterReturn)); + Assert.NotNull(converter); + Assert.True(converter is JsonConverter); + } + } + + [Fact] + public static void GetConverterTypeToConvertNull() + { + Assert.Throws(() => (new JsonSerializerOptions()).GetConverter(typeToConvert: 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 27c4e4c191f278..06747c8778626f 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 @@ -337,9 +337,6 @@ private static void GenericObjectOrJsonElementConverterTestHelper(string conv { var options = new JsonSerializerOptions(); - // Initialize the built-in converters. - JsonSerializer.Serialize("", options); - JsonConverter converter = (JsonConverter)options.GetConverter(typeof(T)); Assert.Equal(converterName, converter.GetType().Name);