From 46a5687394cdcfd182360bd75ae550baf915b796 Mon Sep 17 00:00:00 2001 From: Krzysztof Wicher Date: Mon, 4 Apr 2022 08:34:48 +0200 Subject: [PATCH 1/3] Split Reflection and SourceGen TypeInfos --- .../src/System.Text.Json.csproj | 3 +- .../JsonMetadataServicesConverter.cs | 24 +- .../Object/ObjectDefaultConverter.cs | 8 +- ...ParameterizedConstructorConverter.Large.cs | 10 +- ...ctWithParameterizedConstructorConverter.cs | 8 +- .../JsonSerializer.Read.HandlePropertyName.cs | 2 +- .../JsonSerializer.Read.Helpers.cs | 1 + .../JsonSerializer.Read.Stream.cs | 5 +- .../JsonSerializer.Read.String.cs | 1 + .../JsonSerializer.Read.Utf8JsonReader.cs | 1 + .../JsonSerializer.Write.Helpers.cs | 1 + .../JsonSerializer.Write.Stream.cs | 2 + .../JsonSerializerOptions.Converters.cs | 26 +- .../Serialization/JsonSerializerOptions.cs | 15 +- .../JsonMetadataServices.Collections.cs | 38 +- .../Metadata/JsonMetadataServices.cs | 5 +- .../Metadata/JsonParameterInfo.cs | 2 +- .../Metadata/JsonPropertyInfo.cs | 78 +++- .../Metadata/JsonPropertyInfoOfT.cs | 5 +- .../Metadata/JsonTypeInfo.Cache.cs | 121 +----- .../Serialization/Metadata/JsonTypeInfo.cs | 364 +++++------------- .../Metadata/JsonTypeInfoInternalOfT.cs | 91 ----- .../Serialization/Metadata/JsonTypeInfoOfT.cs | 4 + .../Metadata/ReflectionJsonTypeInfoOfT.cs | 308 +++++++++++++++ .../Metadata/SourceGenJsonTypeInfoOfT.cs | 182 +++++++++ .../Text/Json/Serialization/ReadStack.cs | 6 +- .../Text/Json/Serialization/WriteStack.cs | 4 +- .../Text/Json/ThrowHelper.Serialization.cs | 8 +- .../ReferenceHandlerTests.IgnoreCycles.cs | 1 + .../MixedModeContextTests.cs | 6 +- .../RealWorldContextTests.cs | 26 +- .../Serialization/PropertyVisibilityTests.cs | 24 +- .../SerializationContextTests.cs | 6 +- .../JsonSerializerApiValidation.cs | 8 +- 34 files changed, 789 insertions(+), 605 deletions(-) delete mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoInternalOfT.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/SourceGenJsonTypeInfoOfT.cs diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 2267309134c54..e865ea7500f35 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -122,6 +122,7 @@ System.Text.Json.Nodes.JsonValue + @@ -242,7 +243,7 @@ System.Text.Json.Nodes.JsonValue - + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs index 9a29b32f8c8b6..2b3a9c0d12fbc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs @@ -50,24 +50,7 @@ public JsonMetadataServicesConverter(Func> converterCreator!!, } internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T? value) - { - JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; - - if (_converterStrategy == ConverterStrategy.Object) - { - if (jsonTypeInfo.PropertyCache == null) - { - jsonTypeInfo.InitializePropCache(); - } - - if (jsonTypeInfo.ParameterCache == null && jsonTypeInfo.IsObjectWithParameterizedCtor) - { - jsonTypeInfo.InitializeParameterCache(); - } - } - - return Converter.OnTryRead(ref reader, typeToConvert, options, ref state, out value); - } + => Converter.OnTryRead(ref reader, typeToConvert, options, ref state, out value); internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state) { @@ -84,11 +67,6 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializer return true; } - if (_converterStrategy == ConverterStrategy.Object && jsonTypeInfo.PropertyCache == null) - { - jsonTypeInfo.InitializePropCache(); - } - return Converter.OnTryWrite(writer, value, options, ref state); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs index 6894c4a50fb51..bbe8ec34da206 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs @@ -287,7 +287,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, { // Remember the current property for JsonPath support if an exception is thrown. state.Current.JsonPropertyInfo = jsonPropertyInfo; - state.Current.NumberHandling = jsonPropertyInfo.NumberHandling; + state.Current.NumberHandling = jsonPropertyInfo.EffectiveNumberHandling; bool success = jsonPropertyInfo.GetMemberAndWriteJson(obj, ref state, writer); // Converters only return 'false' when out of data which is not possible in fast path. @@ -303,7 +303,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, { // Remember the current property for JsonPath support if an exception is thrown. state.Current.JsonPropertyInfo = dataExtensionProperty; - state.Current.NumberHandling = dataExtensionProperty.NumberHandling; + state.Current.NumberHandling = dataExtensionProperty.EffectiveNumberHandling; bool success = dataExtensionProperty.GetMemberAndWriteJsonExtensionData(obj, ref state, writer); Debug.Assert(success); @@ -340,7 +340,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, if (jsonPropertyInfo.ShouldSerialize) { state.Current.JsonPropertyInfo = jsonPropertyInfo; - state.Current.NumberHandling = jsonPropertyInfo.NumberHandling; + state.Current.NumberHandling = jsonPropertyInfo.EffectiveNumberHandling; if (!jsonPropertyInfo.GetMemberAndWriteJson(obj!, ref state, writer)) { @@ -370,7 +370,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, { // Remember the current property for JsonPath support if an exception is thrown. state.Current.JsonPropertyInfo = dataExtensionProperty; - state.Current.NumberHandling = dataExtensionProperty.NumberHandling; + state.Current.NumberHandling = dataExtensionProperty.EffectiveNumberHandling; if (!dataExtensionProperty.GetMemberAndWriteJsonExtensionData(obj, ref state, writer)) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs index f820ffbfe3a85..e9160133e4800 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs @@ -52,15 +52,9 @@ protected sealed override void InitializeConstructorArgumentCaches(ref ReadStack { JsonTypeInfo typeInfo = state.Current.JsonTypeInfo; - // Ensure property cache has been initialized. - Debug.Assert(typeInfo.PropertyCache != null); + Debug.Assert(typeInfo.ParameterCache != null); - if (typeInfo.ParameterCache == null) - { - typeInfo.InitializePropCache(); - } - - List> cache = typeInfo.ParameterCache!.List; + List> cache = typeInfo.ParameterCache.List; object?[] arguments = ArrayPool.Shared.Rent(cache.Count); for (int i = 0; i < typeInfo.ParameterCount; i++) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs index 35848ce7fc71a..812283116c04d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs @@ -69,7 +69,7 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo state.Current.JsonPropertyName = propertyNameArray; state.Current.JsonPropertyInfo = jsonPropertyInfo; - state.Current.NumberHandling = jsonPropertyInfo.NumberHandling; + state.Current.NumberHandling = jsonPropertyInfo.EffectiveNumberHandling; bool useExtensionProperty = dataExtKey != null; @@ -505,7 +505,11 @@ private bool ReadConstructorArgumentsWithContinuation(ref ReadStack state, ref U [MethodImpl(MethodImplOptions.AggressiveInlining)] private void BeginRead(ref ReadStack state, ref Utf8JsonReader reader, JsonSerializerOptions options) { - if (state.Current.JsonTypeInfo.ParameterCount != state.Current.JsonTypeInfo.ParameterCache!.Count) + JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; + + jsonTypeInfo.ValidateCanBeUsedForDeserialization(); + + if (jsonTypeInfo.ParameterCount != jsonTypeInfo.ParameterCache!.Count) { ThrowHelper.ThrowInvalidOperationException_ConstructorParameterIncompleteBinding(TypeToConvert); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs index b8a6881941da3..23853b90498d3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs @@ -58,7 +58,7 @@ public static partial class JsonSerializer } state.Current.JsonPropertyInfo = jsonPropertyInfo; - state.Current.NumberHandling = jsonPropertyInfo.NumberHandling; + state.Current.NumberHandling = jsonPropertyInfo.EffectiveNumberHandling; return jsonPropertyInfo; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs index 608e33242e924..29869dda32cfd 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Helpers.cs @@ -31,6 +31,7 @@ public static partial class JsonSerializer var reader = new Utf8JsonReader(utf8Json, isFinalBlock: true, readerState); ReadStack state = default; + jsonTypeInfo.EnsureConfigured(); state.Initialize(jsonTypeInfo); TValue? value; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs index cd8584f03b429..2cc572f03e348 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs @@ -305,6 +305,7 @@ public static partial class JsonSerializer JsonConverter converter = QueueOfTConverter, TValue>.Instance; JsonTypeInfo jsonTypeInfo = CreateQueueJsonTypeInfo(converter, options); ReadStack readStack = default; + jsonTypeInfo.EnsureConfigured(); readStack.Initialize(jsonTypeInfo, supportContinuation: true); var jsonReaderState = new JsonReaderState(options.GetReaderOptions()); @@ -334,7 +335,7 @@ public static partial class JsonSerializer [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Workaround for https://github.com/mono/linker/issues/1416. All usages are marked as unsafe.")] private static JsonTypeInfo CreateQueueJsonTypeInfo(JsonConverter queueConverter, JsonSerializerOptions queueOptions) => - new JsonTypeInfo(typeof(Queue), queueConverter, queueOptions); + new ReflectionJsonTypeInfo>(queueConverter, queueOptions); internal static async ValueTask ReadAllAsync( Stream utf8Json, @@ -344,6 +345,7 @@ public static partial class JsonSerializer JsonSerializerOptions options = jsonTypeInfo.Options; var bufferState = new ReadBufferState(options.DefaultBufferSize); ReadStack readStack = default; + jsonTypeInfo.EnsureConfigured(); readStack.Initialize(jsonTypeInfo, supportContinuation: true); JsonConverter converter = readStack.Current.JsonPropertyInfo!.ConverterBase; var jsonReaderState = new JsonReaderState(options.GetReaderOptions()); @@ -374,6 +376,7 @@ public static partial class JsonSerializer JsonSerializerOptions options = jsonTypeInfo.Options; var bufferState = new ReadBufferState(options.DefaultBufferSize); ReadStack readStack = default; + jsonTypeInfo.EnsureConfigured(); readStack.Initialize(jsonTypeInfo, supportContinuation: true); JsonConverter converter = readStack.Current.JsonPropertyInfo!.ConverterBase; var jsonReaderState = new JsonReaderState(options.GetReaderOptions()); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs index edf1f421d08c9..bc8d2ad0ada80 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.String.cs @@ -307,6 +307,7 @@ public static partial class JsonSerializer private static TValue? ReadFromSpan(ReadOnlySpan json, JsonTypeInfo jsonTypeInfo) { + jsonTypeInfo.EnsureConfigured(); byte[]? tempArray = null; // For performance, avoid obtaining actual byte count unless memory usage is higher than the threshold. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs index 0ff6131735c66..0a8921ff2fd71 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs @@ -218,6 +218,7 @@ public static partial class JsonSerializer private static TValue? Read(ref Utf8JsonReader reader, JsonTypeInfo jsonTypeInfo) { ReadStack state = default; + jsonTypeInfo.EnsureConfigured(); state.Initialize(jsonTypeInfo); JsonReaderState readerState = reader.CurrentState; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs index d756b546725b0..535b91cb29227 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs @@ -64,6 +64,7 @@ private static void WriteUsingSerializer(Utf8JsonWriter writer, in TValu "Incorrect method called. WriteUsingGeneratedSerializer() should have been called instead."); WriteStack state = default; + jsonTypeInfo.EnsureConfigured(); state.Initialize(jsonTypeInfo, supportContinuation: false, supportAsync: false); JsonConverter converter = jsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs index 3690f85696a8e..2adcbf5b8dbd2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs @@ -256,6 +256,7 @@ public static partial class JsonSerializer using (var writer = new Utf8JsonWriter(bufferWriter, writerOptions)) { WriteStack state = new WriteStack { CancellationToken = cancellationToken }; + jsonTypeInfo.EnsureConfigured(); JsonConverter converter = state.Initialize(jsonTypeInfo, supportContinuation: true, supportAsync: true); bool isFinalBlock; @@ -329,6 +330,7 @@ public static partial class JsonSerializer using (var writer = new Utf8JsonWriter(bufferWriter, writerOptions)) { WriteStack state = default; + jsonTypeInfo.EnsureConfigured(); JsonConverter converter = state.Initialize(jsonTypeInfo, supportContinuation: true, supportAsync: false); bool isFinalBlock; 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 478029174db33..846d8908d5cc3 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 @@ -41,7 +41,31 @@ private static void RootReflectionSerializerDependencies() } [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] - static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options) => new JsonTypeInfo(type, options); + static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options) + { + JsonTypeInfo.ValidateType(type, null, null, options); + + MethodInfo methodInfo = typeof(JsonSerializerOptions).GetMethod(nameof(CreateReflectionJsonTypeInfo), BindingFlags.NonPublic | BindingFlags.Instance)!; +#if NETCOREAPP + return (JsonTypeInfo)methodInfo.MakeGenericMethod(type).Invoke(options, BindingFlags.NonPublic | BindingFlags.DoNotWrapExceptions, null, null, null)!; +#else + try + { + return (JsonTypeInfo)methodInfo.MakeGenericMethod(type).Invoke(options, null)!; + } + catch (TargetInvocationException ex) + { + throw ex.InnerException!; + } +#endif + } + } + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + private JsonTypeInfo CreateReflectionJsonTypeInfo() + { + // We do not use Activator.CreateInstance because it will wrap exception if constructor throws it + return new ReflectionJsonTypeInfo(this); } [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] 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 8837b55dcf5da..6cb3cdb0c825f 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 @@ -600,21 +600,22 @@ internal void InitializeForReflectionSerializer() private JsonTypeInfo GetJsonTypeInfoFromContextOrCreate(Type type) { JsonTypeInfo? info = _serializerContext?.GetTypeInfo(type); - if (info != null) + if (info == null && IsInitializedForReflectionSerializer) { - return info; + Debug.Assert( + s_typeInfoCreationFunc != null, + "Reflection-based JsonTypeInfo creator should be initialized if IsInitializedForReflectionSerializer is true."); + info = s_typeInfoCreationFunc(type, this); } - if (!IsInitializedForReflectionSerializer) + if (info == null) { ThrowHelper.ThrowNotSupportedException_NoMetadataForType(type); return null!; } - Debug.Assert( - s_typeInfoCreationFunc != null, - "Reflection-based JsonTypeInfo creator should be initialized if IsInitializedForReflectionSerializer is true."); - return s_typeInfoCreationFunc(type, this); + info.EnsureConfigured(); + return info; } internal JsonDocumentOptions GetDocumentOptions() diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Collections.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Collections.cs index 13393773be79c..ccb09d906eee1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Collections.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Collections.cs @@ -19,7 +19,7 @@ public static partial class JsonMetadataServices /// Serialization metadata for the given type. /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. public static JsonTypeInfo CreateArrayInfo(JsonSerializerOptions options, JsonCollectionInfoValues collectionInfo) - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new ArrayConverter()); @@ -37,7 +37,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonSerializerOptions options, JsonCollectionInfoValues collectionInfo) where TCollection : List - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new ListOfTConverter()); @@ -57,7 +57,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonCollectionInfoValues collectionInfo) where TCollection : Dictionary where TKey : notnull - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new DictionaryOfTKeyTValueConverter()); @@ -83,7 +83,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO Func>, TCollection> createRangeFunc!!) where TCollection : IReadOnlyDictionary where TKey : notnull - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new ImmutableDictionaryOfTKeyTValueConverter(), @@ -104,7 +104,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonCollectionInfoValues collectionInfo) where TCollection : IDictionary where TKey : notnull - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new IDictionaryOfTKeyTValueConverter()); @@ -124,7 +124,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonCollectionInfoValues collectionInfo) where TCollection : IReadOnlyDictionary where TKey : notnull - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new IReadOnlyDictionaryOfTKeyTValueConverter()); @@ -146,7 +146,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonCollectionInfoValues collectionInfo, Func, TCollection> createRangeFunc!!) where TCollection : IEnumerable - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new ImmutableEnumerableOfTConverter(), @@ -164,7 +164,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonSerializerOptions options, JsonCollectionInfoValues collectionInfo) where TCollection : IList - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new IListConverter()); @@ -182,7 +182,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonSerializerOptions options, JsonCollectionInfoValues collectionInfo) where TCollection : IList - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new IListOfTConverter()); @@ -200,7 +200,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonSerializerOptions options, JsonCollectionInfoValues collectionInfo) where TCollection : ISet - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new ISetOfTConverter()); @@ -218,7 +218,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonSerializerOptions options, JsonCollectionInfoValues collectionInfo) where TCollection : ICollection - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new ICollectionOfTConverter()); @@ -236,7 +236,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonSerializerOptions options, JsonCollectionInfoValues collectionInfo) where TCollection : Stack - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new StackOfTConverter()); @@ -254,7 +254,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonSerializerOptions options, JsonCollectionInfoValues collectionInfo) where TCollection : Queue - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new QueueOfTConverter()); @@ -272,7 +272,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonSerializerOptions options, JsonCollectionInfoValues collectionInfo) where TCollection : ConcurrentStack - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new ConcurrentStackOfTConverter()); @@ -290,7 +290,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonSerializerOptions options, JsonCollectionInfoValues collectionInfo) where TCollection : ConcurrentQueue - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new ConcurrentQueueOfTConverter()); @@ -308,7 +308,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonSerializerOptions options, JsonCollectionInfoValues collectionInfo) where TCollection : IEnumerable - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new IEnumerableOfTConverter()); @@ -325,7 +325,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonSerializerOptions options, JsonCollectionInfoValues collectionInfo) where TCollection : IDictionary - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new IDictionaryConverter()); @@ -371,7 +371,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonCollectionInfoValues collectionInfo, Action addFunc!!) where TCollection : IEnumerable - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new StackOrQueueConverter(), @@ -390,7 +390,7 @@ public static JsonTypeInfo CreateArrayInfo(JsonSerializerO JsonSerializerOptions options, JsonCollectionInfoValues collectionInfo) where TCollection : IEnumerable - => new JsonTypeInfoInternal( + => new SourceGenJsonTypeInfo( options, collectionInfo, () => new IEnumerableConverter()); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs index cc044f3693dc7..1184d9c10a08f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.cs @@ -70,7 +70,7 @@ public static JsonPropertyInfo CreatePropertyInfo(JsonSerializerOptions optio /// A instance representing the class or struct. /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. public static JsonTypeInfo CreateObjectInfo(JsonSerializerOptions options!!, JsonObjectInfoValues objectInfo!!) where T : notnull - => new JsonTypeInfoInternal(options, objectInfo); + => new SourceGenJsonTypeInfo(options, objectInfo); /// /// Creates metadata for a primitive or a type with a custom converter. @@ -80,9 +80,8 @@ public static JsonPropertyInfo CreatePropertyInfo(JsonSerializerOptions optio /// This API is for use by the output of the System.Text.Json source generator and should not be called directly. public static JsonTypeInfo CreateValueInfo(JsonSerializerOptions options, JsonConverter converter) { - JsonTypeInfo info = new JsonTypeInfoInternal(converter, options); + JsonTypeInfo info = new SourceGenJsonTypeInfo(converter, options); info.PropertyInfoForTypeInfo = CreateJsonPropertyInfoForClassInfo(typeof(T), info, converter, options); - converter.ConfigureJsonTypeInfo(info, options); return info; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs index afb407e3dc85b..91a25e0b41393 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs @@ -63,7 +63,7 @@ public virtual void Initialize(JsonParameterInfoValues parameterInfo, JsonProper NameAsUtf8Bytes = matchingProperty.NameAsUtf8Bytes!; ConverterBase = matchingProperty.ConverterBase; IgnoreDefaultValuesOnRead = matchingProperty.IgnoreDefaultValuesOnRead; - NumberHandling = matchingProperty.NumberHandling; + NumberHandling = matchingProperty.EffectiveNumberHandling; MatchingPropertyCanBeNull = matchingProperty.PropertyTypeCanBeNull; } 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 86c72cf1e425d..812c500513536 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 @@ -63,14 +63,36 @@ internal static JsonPropertyInfo GetPropertyPlaceholder() internal Type PropertyType { get; set; } = null!; - internal virtual void GetPolicies(JsonIgnoreCondition? ignoreCondition, JsonNumberHandling? declaringTypeNumberHandling) + private bool _isConfigured; + + internal void Configure() { + if (_isConfigured) + { + return; + } + + if (IsIgnored) + { + _isConfigured = true; + return; + } + if (IsForTypeInfo) { - Debug.Assert(MemberInfo == null); - DetermineNumberHandlingForTypeInfo(declaringTypeNumberHandling); + DetermineNumberHandlingForTypeInfo(); } else + { + DetermineNumberHandlingForProperty(); + } + + _isConfigured = true; + } + + internal virtual void GetPolicies(JsonIgnoreCondition? ignoreCondition) + { + if (!IsForTypeInfo) { Debug.Assert(MemberInfo != null); DetermineSerializationCapabilities(ignoreCondition); @@ -84,7 +106,7 @@ internal virtual void GetPolicies(JsonIgnoreCondition? ignoreCondition, JsonNumb } JsonNumberHandlingAttribute? attribute = GetAttribute(MemberInfo); - DetermineNumberHandlingForProperty(attribute?.Handling, declaringTypeNumberHandling); + NumberHandling = attribute?.Handling; } } @@ -214,9 +236,9 @@ internal void DetermineIgnoreCondition(JsonIgnoreCondition? ignoreCondition) #pragma warning restore SYSLIB0020 } - internal void DetermineNumberHandlingForTypeInfo(JsonNumberHandling? numberHandling) + internal void DetermineNumberHandlingForTypeInfo() { - if (numberHandling != null && numberHandling != JsonNumberHandling.Strict && !ConverterBase.IsInternalConverter) + if (DeclaringTypeNumberHandling != null && DeclaringTypeNumberHandling != JsonNumberHandling.Strict && !ConverterBase.IsInternalConverter) { ThrowHelper.ThrowInvalidOperationException_NumberHandlingOnPropertyInvalid(this); } @@ -227,26 +249,24 @@ internal void DetermineNumberHandlingForTypeInfo(JsonNumberHandling? numberHandl // custom collections e.g. public class MyNumberList : List. // Priority 1: Get handling from the type (parent type in this case is the type itself). - NumberHandling = numberHandling; + EffectiveNumberHandling = DeclaringTypeNumberHandling; // Priority 2: Get handling from JsonSerializerOptions instance. - if (!NumberHandling.HasValue && Options.NumberHandling != JsonNumberHandling.Strict) + if (!EffectiveNumberHandling.HasValue && Options.NumberHandling != JsonNumberHandling.Strict) { - NumberHandling = Options.NumberHandling; + EffectiveNumberHandling = Options.NumberHandling; } } } - internal void DetermineNumberHandlingForProperty( - JsonNumberHandling? propertyNumberHandling, - JsonNumberHandling? declaringTypeNumberHandling) + internal void DetermineNumberHandlingForProperty() { bool numberHandlingIsApplicable = NumberHandingIsApplicable(); if (numberHandlingIsApplicable) { // Priority 1: Get handling from attribute on property/field, or its parent class type. - JsonNumberHandling? handling = propertyNumberHandling ?? declaringTypeNumberHandling; + JsonNumberHandling? handling = NumberHandling ?? DeclaringTypeNumberHandling; // Priority 2: Get handling from JsonSerializerOptions instance. if (!handling.HasValue && Options.NumberHandling != JsonNumberHandling.Strict) @@ -254,9 +274,9 @@ internal void DetermineNumberHandlingForTypeInfo(JsonNumberHandling? numberHandl handling = Options.NumberHandling; } - NumberHandling = handling; + EffectiveNumberHandling = handling; } - else if (propertyNumberHandling.HasValue && propertyNumberHandling != JsonNumberHandling.Strict) + else if (NumberHandling.HasValue && NumberHandling != JsonNumberHandling.Strict) { ThrowHelper.ThrowInvalidOperationException_NumberHandlingOnPropertyInvalid(this); } @@ -469,7 +489,20 @@ internal JsonTypeInfo JsonTypeInfo { get { - return _jsonTypeInfo ??= Options.GetOrAddJsonTypeInfo(PropertyType); + if (_jsonTypeInfo != null) + { + // We should not call it on set as it's usually called during initialization + // which is too early to `lock` the JsonTypeInfo + // If this property ever becomes public we should move this to callsites + _jsonTypeInfo.EnsureConfigured(); + } + else + { + // GetOrAddJsonTypeInfo already ensures it's configured. + _jsonTypeInfo = Options.GetOrAddJsonTypeInfo(PropertyType); + } + + return _jsonTypeInfo; } set { @@ -502,8 +535,21 @@ internal JsonTypeInfo JsonTypeInfo /// internal bool SrcGen_IsPublic { get; set; } + /// + /// Number handling for declaring type + /// + internal JsonNumberHandling? DeclaringTypeNumberHandling { get; set; } + + /// + /// Number handling specific to this property, i.e. set by attribute + /// internal JsonNumberHandling? NumberHandling { get; set; } + /// + /// Number handling after considering options and declaring type number handling + /// + internal JsonNumberHandling? EffectiveNumberHandling { get; set; } + // Whether the property type can be null. internal bool PropertyTypeCanBeNull { get; set; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index 8fff88414697c..0f257bd460128 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs @@ -114,7 +114,7 @@ internal sealed class JsonPropertyInfo : JsonPropertyInfo PropertyTypeCanBeNull = PropertyType.CanBeNull(); _propertyTypeEqualsTypeToConvert = typeof(T) == PropertyType; - GetPolicies(ignoreCondition, parentTypeNumberHandling); + GetPolicies(ignoreCondition); } internal void InitializeForSourceGen(JsonSerializerOptions options, JsonPropertyInfoValues propertyInfo) @@ -184,8 +184,7 @@ internal void InitializeForSourceGen(JsonSerializerOptions options, JsonProperty _propertyTypeEqualsTypeToConvert = ConverterBase.TypeToConvert == typeof(T); ConverterStrategy = Converter!.ConverterStrategy; DetermineIgnoreCondition(IgnoreCondition); - // TODO: this method needs to also take the number handling option for the declaring type. - DetermineNumberHandlingForProperty(propertyInfo.NumberHandling, declaringTypeNumberHandling: null); + NumberHandling = propertyInfo.NumberHandling; DetermineSerializationCapabilities(IgnoreCondition); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs index 891285f3f67b6..73bcbeffbffc4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs @@ -51,37 +51,6 @@ public partial class JsonTypeInfo internal Func? CtorParamInitFunc; - internal static JsonPropertyInfo AddProperty( - MemberInfo memberInfo, - Type memberType, - Type parentClassType, - bool isVirtual, - JsonNumberHandling? parentTypeNumberHandling, - JsonSerializerOptions options) - { - JsonIgnoreCondition? ignoreCondition = JsonPropertyInfo.GetAttribute(memberInfo)?.Condition; - if (ignoreCondition == JsonIgnoreCondition.Always) - { - return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(memberInfo, memberType, isVirtual, options); - } - - JsonConverter converter = GetConverter( - memberType, - parentClassType, - memberInfo, - options); - - return CreateProperty( - declaredPropertyType: memberType, - memberInfo, - parentClassType, - isVirtual, - converter, - options, - parentTypeNumberHandling, - ignoreCondition); - } - internal static JsonPropertyInfo CreateProperty( Type declaredPropertyType, MemberInfo? memberInfo, @@ -113,7 +82,7 @@ public partial class JsonTypeInfo /// Create a for a given Type. /// See . /// - internal static JsonPropertyInfo CreatePropertyInfoForTypeInfo( + private static JsonPropertyInfo CreatePropertyInfoForTypeInfo( Type declaredPropertyType, JsonConverter converter, JsonNumberHandling? numberHandling, @@ -142,6 +111,7 @@ public partial class JsonTypeInfo { PropertyRef propertyRef; + ValidateCanBeUsedForDeserialization(); ulong key = GetKey(propertyName); // Keep a local copy of the cache in case it changes by another thread. @@ -569,92 +539,5 @@ internal void UpdateSortedParameterCache(ref ReadStackFrame frame) frame.CtorArgumentState.ParameterRefCache = null; } - - internal void InitializePropCache() - { - Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Object); - - // Delayed JsonTypeInfo initialization can be invoked by - // multiple threads, add a temporary check to avoid contention. - // TODO refactor so that metadata initialization is single threaded. - if (Volatile.Read(ref PropertyCache) is not null) - { - return; - } - - JsonSerializerContext? context = Options.JsonSerializerContext; - Debug.Assert(context != null); - - JsonPropertyInfo[] array; - if (PropInitFunc == null || (array = PropInitFunc(context)) == null) - { - ThrowHelper.ThrowInvalidOperationException_NoMetadataForTypeProperties(context, Type); - return; - } - - Dictionary? ignoredMembers = null; - JsonPropertyDictionary propertyCache = new(Options.PropertyNameCaseInsensitive, array.Length); - - for (int i = 0; i < array.Length; i++) - { - JsonPropertyInfo jsonPropertyInfo = array[i]; - bool hasJsonInclude = jsonPropertyInfo.SrcGen_HasJsonInclude; - - if (!jsonPropertyInfo.SrcGen_IsPublic) - { - if (hasJsonInclude) - { - ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(jsonPropertyInfo.ClrName!, jsonPropertyInfo.DeclaringType); - } - - continue; - } - - if (jsonPropertyInfo.MemberType == MemberTypes.Field && !hasJsonInclude && !Options.IncludeFields) - { - continue; - } - - if (jsonPropertyInfo.SrcGen_IsExtensionData) - { - Debug.Assert(IsValidDataExtensionProperty(jsonPropertyInfo)); - - DataExtensionProperty = jsonPropertyInfo; - continue; - } - - CacheMember(jsonPropertyInfo, propertyCache, ref ignoredMembers); - } - - // Populate a local cache and assign it to the global cache after completion. - Volatile.Write(ref PropertyCache, propertyCache); - } - - internal void InitializeParameterCache() - { - Debug.Assert(PropertyCache != null); - Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Object); - - // Delayed JsonTypeInfo initialization can be invoked by - // multiple threads, add a temporary check to avoid contention. - // TODO refactor so that metadata initialization is single threaded. - if (Volatile.Read(ref ParameterCache) is not null) - { - return; - } - - JsonSerializerContext? context = Options.JsonSerializerContext; - Debug.Assert(context != null); - - JsonParameterInfoValues[] array; - if (CtorParamInitFunc == null || (array = CtorParamInitFunc()) == null) - { - ThrowHelper.ThrowInvalidOperationException_NoMetadataForTypeCtorParams(context, Type); - return; - } - - InitializeConstructorParameters(array, sourceGenMode: true); - Debug.Assert(ParameterCache != null); - } } } 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 f8eb263bc0753..275343b55a1b5 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 @@ -6,7 +6,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Text.Json.Reflection; +using System.Runtime.CompilerServices; using System.Threading; namespace System.Text.Json.Serialization.Metadata @@ -32,7 +32,7 @@ public partial class JsonTypeInfo // Add method delegate for non-generic Stack and Queue; and types that derive from them. internal object? AddMethodDelegate { get; set; } - internal JsonPropertyInfo? DataExtensionProperty { get; private set; } + internal JsonPropertyInfo? DataExtensionProperty { get; set; } // If enumerable or dictionary, the JsonTypeInfo for the element type. private JsonTypeInfo? _elementTypeInfo; @@ -40,6 +40,19 @@ public partial class JsonTypeInfo // Avoids having to perform an expensive cast to JsonTypeInfo to check if there is a Serialize method. internal bool HasSerialize { get; set; } + // Configure would normally have thrown why initializing properties for source gen but type had SerializeHandler + // so it is allowed to be used for serialization but it will throw if used for deserialization + internal bool ThrowOnDeserialize { get; set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void ValidateCanBeUsedForDeserialization() + { + if (ThrowOnDeserialize) + { + ThrowHelper.ThrowInvalidOperationException_NoMetadataForTypeProperties(Options.JsonSerializerContext, Type); + } + } + /// /// Return the JsonTypeInfo for the element type, or null if the type is not an enumerable or dictionary. /// @@ -51,9 +64,18 @@ public partial class JsonTypeInfo { get { - if (_elementTypeInfo == null && ElementType != null) + if (_elementTypeInfo == null) { - _elementTypeInfo = Options.GetOrAddJsonTypeInfo(ElementType); + if (ElementType != null) + { + // GetOrAddJsonTypeInfo already ensures JsonTypeInfo is configured + // also see comment on JsonPropertyInfo.JsonTypeInfo + _elementTypeInfo = Options.GetOrAddJsonTypeInfo(ElementType); + } + } + else + { + _elementTypeInfo.EnsureConfigured(); } return _elementTypeInfo; @@ -82,11 +104,20 @@ public partial class JsonTypeInfo { get { - if (_keyTypeInfo == null && KeyType != null) + if (_keyTypeInfo == null) { - Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Dictionary); + if (KeyType != null) + { + Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Dictionary); - _keyTypeInfo = Options.GetOrAddJsonTypeInfo(KeyType); + // GetOrAddJsonTypeInfo already ensures JsonTypeInfo is configured + // also see comment on JsonPropertyInfo.JsonTypeInfo + _keyTypeInfo = Options.GetOrAddJsonTypeInfo(KeyType); + } + } + else + { + _keyTypeInfo.EnsureConfigured(); } return _keyTypeInfo; @@ -123,9 +154,6 @@ public partial class JsonTypeInfo /// internal JsonPropertyInfo PropertyInfoForTypeInfo { get; set; } - internal bool IsObjectWithParameterizedCtor => PropertyInfoForTypeInfo.ConverterBase.ConstructorIsParameterized; - - /// /// Returns a helper class used for computing the default value. /// @@ -147,40 +175,21 @@ internal JsonTypeInfo(Type type, JsonSerializerOptions options!!, bool dummy) PropertyInfoForTypeInfo = null!; } - [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] - internal JsonTypeInfo(Type type, JsonSerializerOptions options) : - this( - type, - GetConverter( - type, - parentClassType: null, // A TypeInfo never has a "parent" class. - memberInfo: null, // A TypeInfo never has a "parent" property. - options), - options) - { - } - - [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] internal JsonTypeInfo(Type type, JsonConverter converter, JsonSerializerOptions options) { Type = type; Options = options; - NumberHandling = GetNumberHandlingForType(Type); PropertyInfoForTypeInfo = CreatePropertyInfoForTypeInfo(Type, converter, NumberHandling, Options); ElementType = converter.ElementType; switch (PropertyInfoForTypeInfo.ConverterStrategy) { - case ConverterStrategy.Object: - { - AddPropertiesAndParametersUsingReflection(); - } - break; case ConverterStrategy.Dictionary: { KeyType = converter.KeyType; } break; + case ConverterStrategy.Object: case ConverterStrategy.Enumerable: case ConverterStrategy.Value: break; @@ -193,170 +202,59 @@ internal JsonTypeInfo(Type type, JsonConverter converter, JsonSerializerOptions Debug.Fail($"Unexpected class type: {PropertyInfoForTypeInfo.ConverterStrategy}"); throw new InvalidOperationException(); } - - CreateObject = Options.MemberAccessorStrategy.CreateConstructor(type); - - // These two method overrides are expected to perform - // orthogonal changes, so we can invoke them both safely. - converter.ConfigureJsonTypeInfo(this, options); - converter.ConfigureJsonTypeInfoUsingReflection(this, options); } - [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] - private void AddPropertiesAndParametersUsingReflection() - { - Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Object); + private bool _isConfigured; - const BindingFlags bindingFlags = - BindingFlags.Instance | - BindingFlags.Public | - BindingFlags.NonPublic | - BindingFlags.DeclaredOnly; - Dictionary? ignoredMembers = null; + internal void EnsureConfigured() + { + if (_isConfigured) + return; - PropertyInfo[] properties = Type.GetProperties(bindingFlags); + Configure(); - bool propertyOrderSpecified = false; + _isConfigured = true; + } - // PropertyCache is not accessed by other threads until the current JsonTypeInfo instance - // is finished initializing and added to the cache on JsonSerializerOptions. - // Default 'capacity' to the common non-polymorphic + property case. - PropertyCache = new JsonPropertyDictionary(Options.PropertyNameCaseInsensitive, capacity: properties.Length); + internal virtual void Configure() + { + JsonConverter converter = PropertyInfoForTypeInfo.ConverterBase; + converter.ConfigureJsonTypeInfo(this, Options); + PropertyInfoForTypeInfo.DeclaringTypeNumberHandling = NumberHandling; + PropertyInfoForTypeInfo.Configure(); - // We start from the most derived type. - Type? currentType = Type; + // Source gen currently when initializes properties + // also assigns JsonPropertyInfo's JsonTypeInfo which causes SO if there are any + // cycles in the object graph. For that reason properties cannot be added immediately. + // This is a no-op for ReflectionJsonTypeInfo + LateAddProperties(); - while (true) + if (converter.ConverterStrategy == ConverterStrategy.Object && PropertyCache != null) { - foreach (PropertyInfo propertyInfo in properties) - { - bool isVirtual = propertyInfo.IsVirtual(); - string propertyName = propertyInfo.Name; - - // Ignore indexers and virtual properties that have overrides that were [JsonIgnore]d. - if (propertyInfo.GetIndexParameters().Length > 0 || - PropertyIsOverridenAndIgnored(propertyName, propertyInfo.PropertyType, isVirtual, ignoredMembers)) - { - continue; - } - - // For now we only support public properties (i.e. setter and/or getter is public). - if (propertyInfo.GetMethod?.IsPublic == true || - propertyInfo.SetMethod?.IsPublic == true) - { - CacheMember( - currentType, - propertyInfo.PropertyType, - propertyInfo, - isVirtual, - NumberHandling, - ref propertyOrderSpecified, - ref ignoredMembers); - } - else - { - if (JsonPropertyInfo.GetAttribute(propertyInfo) != null) - { - ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(propertyName, currentType); - } - - // Non-public properties should not be included for (de)serialization. - } - } - - foreach (FieldInfo fieldInfo in currentType.GetFields(bindingFlags)) + foreach (var jsonPropertyInfoKv in PropertyCache.List) { - string fieldName = fieldInfo.Name; - - if (PropertyIsOverridenAndIgnored(fieldName, fieldInfo.FieldType, currentMemberIsVirtual: false, ignoredMembers)) - { - continue; - } - - bool hasJsonInclude = JsonPropertyInfo.GetAttribute(fieldInfo) != null; - - if (fieldInfo.IsPublic) - { - if (hasJsonInclude || Options.IncludeFields) - { - CacheMember( - currentType, - fieldInfo.FieldType, - fieldInfo, - isVirtual: false, - NumberHandling, - ref propertyOrderSpecified, - ref ignoredMembers); - } - } - else - { - if (hasJsonInclude) - { - ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(fieldName, currentType); - } - - // Non-public fields should not be included for (de)serialization. - } + JsonPropertyInfo jsonPropertyInfo = jsonPropertyInfoKv.Value!; + jsonPropertyInfo.DeclaringTypeNumberHandling = NumberHandling; + jsonPropertyInfo.Configure(); } - currentType = currentType.BaseType; - if (currentType == null) + if (converter.ConstructorIsParameterized) { - break; + InitializeConstructorParameters(GetParameterInfoValues(), sourceGenMode: Options.JsonSerializerContext != null); } - - properties = currentType.GetProperties(bindingFlags); - }; - - if (propertyOrderSpecified) - { - PropertyCache.List.Sort((p1, p2) => p1.Value!.Order.CompareTo(p2.Value!.Order)); - } - - JsonConverter converter = PropertyInfoForTypeInfo.ConverterBase; - if (converter.ConstructorIsParameterized) - { - ParameterInfo[] parameters = converter.ConstructorInfo!.GetParameters(); - int parameterCount = parameters.Length; - - JsonParameterInfoValues[] jsonParameters = GetParameterInfoArray(parameters); - InitializeConstructorParameters(jsonParameters); } } - private void CacheMember( - Type declaringType, - Type memberType, - MemberInfo memberInfo, - bool isVirtual, - JsonNumberHandling? typeNumberHandling, - ref bool propertyOrderSpecified, - ref Dictionary? ignoredMembers) - { - bool hasExtensionAttribute = memberInfo.GetCustomAttribute(typeof(JsonExtensionDataAttribute)) != null; - if (hasExtensionAttribute && DataExtensionProperty != null) - { - ThrowHelper.ThrowInvalidOperationException_SerializationDuplicateTypeAttribute(Type, typeof(JsonExtensionDataAttribute)); - } - - JsonPropertyInfo jsonPropertyInfo = AddProperty(memberInfo, memberType, declaringType, isVirtual, typeNumberHandling, Options); - Debug.Assert(jsonPropertyInfo.NameAsString != null); + internal virtual void LateAddProperties() { } - if (hasExtensionAttribute) - { - Debug.Assert(DataExtensionProperty == null); - ValidateAndAssignDataExtensionProperty(jsonPropertyInfo); - Debug.Assert(DataExtensionProperty != null); - } - else - { - CacheMember(jsonPropertyInfo, PropertyCache, ref ignoredMembers); - propertyOrderSpecified |= jsonPropertyInfo.Order != 0; - } + internal virtual JsonParameterInfoValues[] GetParameterInfoValues() + { + // If JsonTypeInfo becomes abstract this should be abstract as well + Debug.Fail("This should never be called."); + return null!; } - private void CacheMember(JsonPropertyInfo jsonPropertyInfo, JsonPropertyDictionary? propertyCache, ref Dictionary? ignoredMembers) + internal void CacheMember(JsonPropertyInfo jsonPropertyInfo, JsonPropertyDictionary? propertyCache, ref Dictionary? ignoredMembers) { string memberName = jsonPropertyInfo.ClrName!; @@ -428,7 +326,7 @@ public ParameterLookupValue(JsonPropertyInfo jsonPropertyInfo) public JsonPropertyInfo JsonPropertyInfo { get; } } - private void InitializeConstructorParameters(JsonParameterInfoValues[] jsonParameters, bool sourceGenMode = false) + internal void InitializeConstructorParameters(JsonParameterInfoValues[] jsonParameters, bool sourceGenMode = false) { var parameterCache = new JsonPropertyDictionary(Options.PropertyNameCaseInsensitive, jsonParameters.Length); @@ -489,57 +387,44 @@ private void InitializeConstructorParameters(JsonParameterInfoValues[] jsonParam Volatile.Write(ref ParameterCache, parameterCache); } - private static JsonParameterInfoValues[] GetParameterInfoArray(ParameterInfo[] parameters) + internal static void ValidateType(Type type, Type? parentClassType, MemberInfo? memberInfo, JsonSerializerOptions options) { - int parameterCount = parameters.Length; - JsonParameterInfoValues[] jsonParameters = new JsonParameterInfoValues[parameterCount]; - - for (int i = 0; i < parameterCount; i++) + if (IsInvalidForSerialization(type)) { - ParameterInfo reflectionInfo = parameters[i]; - - JsonParameterInfoValues jsonInfo = new() - { - Name = reflectionInfo.Name!, - ParameterType = reflectionInfo.ParameterType, - Position = reflectionInfo.Position, - HasDefaultValue = reflectionInfo.HasDefaultValue, - DefaultValue = reflectionInfo.GetDefaultValue() - }; - - jsonParameters[i] = jsonInfo; + ThrowHelper.ThrowInvalidOperationException_CannotSerializeInvalidType(type, parentClassType, memberInfo); } + } - return jsonParameters; + internal static bool IsInvalidForSerialization(Type type) + { + return type.IsPointer || IsByRefLike(type) || type.ContainsGenericParameters; } - private static bool PropertyIsOverridenAndIgnored( - string currentMemberName, - Type currentMemberType, - bool currentMemberIsVirtual, - Dictionary? ignoredMembers) + private static bool IsByRefLike(Type type) { - if (ignoredMembers == null || !ignoredMembers.TryGetValue(currentMemberName, out JsonPropertyInfo? ignoredMember)) +#if BUILDING_INBOX_LIBRARY + return type.IsByRefLike; +#else + if (!type.IsValueType) { return false; } - return currentMemberType == ignoredMember.PropertyType && - currentMemberIsVirtual && - ignoredMember.IsVirtual; - } + object[] attributes = type.GetCustomAttributes(inherit: false); - private void ValidateAndAssignDataExtensionProperty(JsonPropertyInfo jsonPropertyInfo) - { - if (!IsValidDataExtensionProperty(jsonPropertyInfo)) + for (int i = 0; i < attributes.Length; i++) { - ThrowHelper.ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(Type, jsonPropertyInfo); + if (attributes[i].GetType().FullName == "System.Runtime.CompilerServices.IsByRefLikeAttribute") + { + return true; + } } - DataExtensionProperty = jsonPropertyInfo; + return false; +#endif } - private bool IsValidDataExtensionProperty(JsonPropertyInfo jsonPropertyInfo) + internal bool IsValidDataExtensionProperty(JsonPropertyInfo jsonPropertyInfo) { Type memberType = jsonPropertyInfo.PropertyType; @@ -570,67 +455,6 @@ private bool IsValidDataExtensionProperty(JsonPropertyInfo jsonPropertyInfo) return jsonParameterInfo; } - // This method gets the runtime information for a given type or property. - // The runtime information consists of the following: - // - class type, - // - element type (if the type is a collection), - // - the converter (either native or custom), if one exists. - private static JsonConverter GetConverter( - Type type, - Type? parentClassType, - MemberInfo? memberInfo, - JsonSerializerOptions options) - { - Debug.Assert(type != null); - ValidateType(type, parentClassType, memberInfo, options); - return options.GetConverterFromMember(parentClassType, type, memberInfo); - } - - private static void ValidateType(Type type, Type? parentClassType, MemberInfo? memberInfo, JsonSerializerOptions options) - { - if (!options.IsJsonTypeInfoCached(type) && IsInvalidForSerialization(type)) - { - ThrowHelper.ThrowInvalidOperationException_CannotSerializeInvalidType(type, parentClassType, memberInfo); - } - } - - private static bool IsInvalidForSerialization(Type type) - { - return type.IsPointer || IsByRefLike(type) || type.ContainsGenericParameters; - } - - private static bool IsByRefLike(Type type) - { -#if BUILDING_INBOX_LIBRARY - return type.IsByRefLike; -#else - if (!type.IsValueType) - { - return false; - } - - object[] attributes = type.GetCustomAttributes(inherit: false); - - for (int i = 0; i < attributes.Length; i++) - { - if (attributes[i].GetType().FullName == "System.Runtime.CompilerServices.IsByRefLikeAttribute") - { - return true; - } - } - - return false; -#endif - } - - private static JsonNumberHandling? GetNumberHandlingForType(Type type) - { - var numberHandlingAttribute = - (JsonNumberHandlingAttribute?)JsonSerializerOptions.GetAttributeThatCanHaveMultiple(type, typeof(JsonNumberHandlingAttribute)); - - return numberHandlingAttribute?.Handling; - } - [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => $"ConverterStrategy.{PropertyInfoForTypeInfo.ConverterStrategy}, {Type.Name}"; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoInternalOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoInternalOfT.cs deleted file mode 100644 index bbb899e9b92ff..0000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoInternalOfT.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text.Json.Serialization.Converters; - -namespace System.Text.Json.Serialization.Metadata -{ - /// - /// Creates and initializes serialization metadata for a type. - /// - /// - internal sealed class JsonTypeInfoInternal : JsonTypeInfo - { - /// - /// Creates serialization metadata for a type using a simple converter. - /// - public JsonTypeInfoInternal(JsonConverter converter, JsonSerializerOptions options) - : base(typeof(T), options) - { - ElementType = converter.ElementType; - } - - /// - /// Creates serialization metadata for an object. - /// - public JsonTypeInfoInternal(JsonSerializerOptions options, JsonObjectInfoValues objectInfo) : base(typeof(T), options) - { -#pragma warning disable CS8714 - // The type cannot be used as type parameter in the generic type or method. - // Nullability of type argument doesn't match 'notnull' constraint. - JsonConverter converter; - - if (objectInfo.ObjectWithParameterizedConstructorCreator != null) - { - converter = new JsonMetadataServicesConverter( - () => new LargeObjectWithParameterizedConstructorConverter(), - ConverterStrategy.Object); - CreateObjectWithArgs = objectInfo.ObjectWithParameterizedConstructorCreator; - CtorParamInitFunc = objectInfo.ConstructorParameterMetadataInitializer; - } - else - { - converter = new JsonMetadataServicesConverter(() => new ObjectDefaultConverter(), ConverterStrategy.Object); - SetCreateObjectFunc(objectInfo.ObjectCreator); - } -#pragma warning restore CS8714 - - PropInitFunc = objectInfo.PropertyMetadataInitializer; - ElementType = converter.ElementType; - SerializeHandler = objectInfo.SerializeHandler; - PropertyInfoForTypeInfo = JsonMetadataServices.CreateJsonPropertyInfoForClassInfo(typeof(T), this, converter, Options); - NumberHandling = objectInfo.NumberHandling; - converter.ConfigureJsonTypeInfo(this, Options); - } - - /// - /// Creates serialization metadata for a collection. - /// - public JsonTypeInfoInternal( - JsonSerializerOptions options, - JsonCollectionInfoValues collectionInfo!!, - Func> converterCreator, - object? createObjectWithArgs = null, - object? addFunc = null) - : base(typeof(T), options) - { - ConverterStrategy strategy = collectionInfo.KeyInfo == null ? ConverterStrategy.Enumerable : ConverterStrategy.Dictionary; - JsonConverter converter = new JsonMetadataServicesConverter(converterCreator, strategy); - - KeyType = converter.KeyType; - ElementType = converter.ElementType; - KeyTypeInfo = collectionInfo.KeyInfo; - ElementTypeInfo = collectionInfo.ElementInfo ?? throw new ArgumentNullException(nameof(collectionInfo.ElementInfo)); - NumberHandling = collectionInfo.NumberHandling; - PropertyInfoForTypeInfo = JsonMetadataServices.CreateJsonPropertyInfoForClassInfo(typeof(T), this, converter, options); - SerializeHandler = collectionInfo.SerializeHandler; - CreateObjectWithArgs = createObjectWithArgs; - AddMethodDelegate = addFunc; - SetCreateObjectFunc(collectionInfo.ObjectCreator); - converter.ConfigureJsonTypeInfo(this, Options); - } - - private void SetCreateObjectFunc(Func? createObjectFunc) - { - if (createObjectFunc != null) - { - CreateObject = () => createObjectFunc(); - } - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs index e09f840c2c0f5..fec8cbe6bc40b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs @@ -19,6 +19,10 @@ public abstract class JsonTypeInfo : JsonTypeInfo base(type, options, dummy: false) { } + internal JsonTypeInfo(JsonConverter converter, JsonSerializerOptions options) + : base(typeof(T), converter, options) + { } + internal JsonTypeInfo() { Debug.Assert(false, "This constructor should not be called."); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs new file mode 100644 index 0000000000000..5db3e52a89e83 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs @@ -0,0 +1,308 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.Json.Reflection; + +namespace System.Text.Json.Serialization.Metadata +{ + /// + /// Provides JSON serialization-related metadata about a type. + /// + internal class ReflectionJsonTypeInfo : JsonTypeInfo + { + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + internal ReflectionJsonTypeInfo(JsonSerializerOptions options) + : this( + GetConverter( + typeof(T), + parentClassType: null, // A TypeInfo never has a "parent" class. + memberInfo: null, // A TypeInfo never has a "parent" property. + options), + options) + { + } + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + internal ReflectionJsonTypeInfo(JsonConverter converter, JsonSerializerOptions options) + : base(converter, options) + { + NumberHandling = GetNumberHandlingForType(Type); + + if (PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Object) + { + AddPropertiesAndParametersUsingReflection(); + } + + CreateObject = Options.MemberAccessorStrategy.CreateConstructor(typeof(T)); + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "Constructor is marked as RequiresUnreferencedCode")] + internal override void Configure() + { + base.Configure(); + PropertyInfoForTypeInfo.ConverterBase.ConfigureJsonTypeInfoUsingReflection(this, Options); + } + + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + private void AddPropertiesAndParametersUsingReflection() + { + Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Object); + + const BindingFlags bindingFlags = + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic | + BindingFlags.DeclaredOnly; + + Dictionary? ignoredMembers = null; + PropertyInfo[] properties = Type.GetProperties(bindingFlags); + bool propertyOrderSpecified = false; + + // PropertyCache is not accessed by other threads until the current JsonTypeInfo instance + // is finished initializing and added to the cache on JsonSerializerOptions. + // Default 'capacity' to the common non-polymorphic + property case. + PropertyCache = new JsonPropertyDictionary(Options.PropertyNameCaseInsensitive, capacity: properties.Length); + + // We start from the most derived type. + Type? currentType = Type; + + while (true) + { + foreach (PropertyInfo propertyInfo in properties) + { + bool isVirtual = propertyInfo.IsVirtual(); + string propertyName = propertyInfo.Name; + + // Ignore indexers and virtual properties that have overrides that were [JsonIgnore]d. + if (propertyInfo.GetIndexParameters().Length > 0 || + PropertyIsOverridenAndIgnored(propertyName, propertyInfo.PropertyType, isVirtual, ignoredMembers)) + { + continue; + } + + // For now we only support public properties (i.e. setter and/or getter is public). + if (propertyInfo.GetMethod?.IsPublic == true || + propertyInfo.SetMethod?.IsPublic == true) + { + CacheMember( + currentType, + propertyInfo.PropertyType, + propertyInfo, + isVirtual, + NumberHandling, + ref propertyOrderSpecified, + ref ignoredMembers); + } + else + { + if (JsonPropertyInfo.GetAttribute(propertyInfo) != null) + { + ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(propertyName, currentType); + } + + // Non-public properties should not be included for (de)serialization. + } + } + + foreach (FieldInfo fieldInfo in currentType.GetFields(bindingFlags)) + { + string fieldName = fieldInfo.Name; + + if (PropertyIsOverridenAndIgnored(fieldName, fieldInfo.FieldType, currentMemberIsVirtual: false, ignoredMembers)) + { + continue; + } + + bool hasJsonInclude = JsonPropertyInfo.GetAttribute(fieldInfo) != null; + + if (fieldInfo.IsPublic) + { + if (hasJsonInclude || Options.IncludeFields) + { + CacheMember( + currentType, + fieldInfo.FieldType, + fieldInfo, + isVirtual: false, + NumberHandling, + ref propertyOrderSpecified, + ref ignoredMembers); + } + } + else + { + if (hasJsonInclude) + { + ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(fieldName, currentType); + } + + // Non-public fields should not be included for (de)serialization. + } + } + + currentType = currentType.BaseType; + if (currentType == null) + { + break; + } + + properties = currentType.GetProperties(bindingFlags); + }; + + if (propertyOrderSpecified) + { + PropertyCache.List.Sort((p1, p2) => p1.Value!.Order.CompareTo(p2.Value!.Order)); + } + } + + private void CacheMember( + Type declaringType, + Type memberType, + MemberInfo memberInfo, + bool isVirtual, + JsonNumberHandling? typeNumberHandling, + ref bool propertyOrderSpecified, + ref Dictionary? ignoredMembers) + { + bool hasExtensionAttribute = memberInfo.GetCustomAttribute(typeof(JsonExtensionDataAttribute)) != null; + if (hasExtensionAttribute && DataExtensionProperty != null) + { + ThrowHelper.ThrowInvalidOperationException_SerializationDuplicateTypeAttribute(Type, typeof(JsonExtensionDataAttribute)); + } + + JsonPropertyInfo jsonPropertyInfo = AddProperty(memberInfo, memberType, declaringType, isVirtual, typeNumberHandling, Options); + Debug.Assert(jsonPropertyInfo.NameAsString != null); + + if (hasExtensionAttribute) + { + Debug.Assert(DataExtensionProperty == null); + ValidateAndAssignDataExtensionProperty(jsonPropertyInfo); + Debug.Assert(DataExtensionProperty != null); + } + else + { + CacheMember(jsonPropertyInfo, PropertyCache, ref ignoredMembers); + propertyOrderSpecified |= jsonPropertyInfo.Order != 0; + } + } + + private static JsonPropertyInfo AddProperty( + MemberInfo memberInfo, + Type memberType, + Type parentClassType, + bool isVirtual, + JsonNumberHandling? parentTypeNumberHandling, + JsonSerializerOptions options) + { + JsonIgnoreCondition? ignoreCondition = JsonPropertyInfo.GetAttribute(memberInfo)?.Condition; + if (ignoreCondition == JsonIgnoreCondition.Always) + { + return JsonPropertyInfo.CreateIgnoredPropertyPlaceholder(memberInfo, memberType, isVirtual, options); + } + + ValidateType(memberType, parentClassType, memberInfo, options); + + JsonConverter converter = GetConverter( + memberType, + parentClassType, + memberInfo, + options); + + return CreateProperty( + declaredPropertyType: memberType, + memberInfo, + parentClassType, + isVirtual, + converter, + options, + parentTypeNumberHandling, + ignoreCondition); + } + + private static JsonNumberHandling? GetNumberHandlingForType(Type type) + { + var numberHandlingAttribute = + (JsonNumberHandlingAttribute?)JsonSerializerOptions.GetAttributeThatCanHaveMultiple(type, typeof(JsonNumberHandlingAttribute)); + + return numberHandlingAttribute?.Handling; + } + + // This method gets the runtime information for a given type or property. + // The runtime information consists of the following: + // - class type, + // - element type (if the type is a collection), + // - the converter (either native or custom), if one exists. + private static JsonConverter GetConverter( + Type type, + Type? parentClassType, + MemberInfo? memberInfo, + JsonSerializerOptions options) + { + Debug.Assert(type != null); + Debug.Assert(!IsInvalidForSerialization(type), $"Type `{type.FullName}` should already be validated."); + return options.GetConverterFromMember(parentClassType, type, memberInfo); + } + + private static bool PropertyIsOverridenAndIgnored( + string currentMemberName, + Type currentMemberType, + bool currentMemberIsVirtual, + Dictionary? ignoredMembers) + { + if (ignoredMembers == null || !ignoredMembers.TryGetValue(currentMemberName, out JsonPropertyInfo? ignoredMember)) + { + return false; + } + + return currentMemberType == ignoredMember.PropertyType && + currentMemberIsVirtual && + ignoredMember.IsVirtual; + } + + internal override JsonParameterInfoValues[] GetParameterInfoValues() + { + ParameterInfo[] parameters = PropertyInfoForTypeInfo.ConverterBase.ConstructorInfo!.GetParameters(); + return GetParameterInfoArray(parameters); + } + + private static JsonParameterInfoValues[] GetParameterInfoArray(ParameterInfo[] parameters) + { + int parameterCount = parameters.Length; + JsonParameterInfoValues[] jsonParameters = new JsonParameterInfoValues[parameterCount]; + + for (int i = 0; i < parameterCount; i++) + { + ParameterInfo reflectionInfo = parameters[i]; + + JsonParameterInfoValues jsonInfo = new() + { + Name = reflectionInfo.Name!, + ParameterType = reflectionInfo.ParameterType, + Position = reflectionInfo.Position, + HasDefaultValue = reflectionInfo.HasDefaultValue, + DefaultValue = reflectionInfo.GetDefaultValue() + }; + + jsonParameters[i] = jsonInfo; + } + + return jsonParameters; + } + + private void ValidateAndAssignDataExtensionProperty(JsonPropertyInfo jsonPropertyInfo) + { + if (!IsValidDataExtensionProperty(jsonPropertyInfo)) + { + ThrowHelper.ThrowInvalidOperationException_SerializationDataExtensionPropertyInvalid(Type, jsonPropertyInfo); + } + + DataExtensionProperty = jsonPropertyInfo; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/SourceGenJsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/SourceGenJsonTypeInfoOfT.cs new file mode 100644 index 0000000000000..6dc1a5b17d231 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/SourceGenJsonTypeInfoOfT.cs @@ -0,0 +1,182 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Text.Json.Serialization.Converters; + +namespace System.Text.Json.Serialization.Metadata +{ + /// + /// Creates and initializes serialization metadata for a type. + /// + /// + internal sealed class SourceGenJsonTypeInfo : JsonTypeInfo + { + /// + /// Creates serialization metadata for a type using a simple converter. + /// + public SourceGenJsonTypeInfo(JsonConverter converter, JsonSerializerOptions options) + : base(typeof(T), options) + { + ElementType = converter.ElementType; + } + + /// + /// Creates serialization metadata for an object. + /// + public SourceGenJsonTypeInfo(JsonSerializerOptions options, JsonObjectInfoValues objectInfo) : base(typeof(T), options) + { +#pragma warning disable CS8714 + // The type cannot be used as type parameter in the generic type or method. + // Nullability of type argument doesn't match 'notnull' constraint. + JsonConverter converter; + + if (objectInfo.ObjectWithParameterizedConstructorCreator != null) + { + converter = new JsonMetadataServicesConverter( + () => new LargeObjectWithParameterizedConstructorConverter(), + ConverterStrategy.Object); + CreateObjectWithArgs = objectInfo.ObjectWithParameterizedConstructorCreator; + CtorParamInitFunc = objectInfo.ConstructorParameterMetadataInitializer; + } + else + { + converter = new JsonMetadataServicesConverter(() => new ObjectDefaultConverter(), ConverterStrategy.Object); + SetCreateObjectFunc(objectInfo.ObjectCreator); + } +#pragma warning restore CS8714 + + PropInitFunc = objectInfo.PropertyMetadataInitializer; + ElementType = converter.ElementType; + SerializeHandler = objectInfo.SerializeHandler; + PropertyInfoForTypeInfo = JsonMetadataServices.CreateJsonPropertyInfoForClassInfo(typeof(T), this, converter, Options); + NumberHandling = objectInfo.NumberHandling; + } + + /// + /// Creates serialization metadata for a collection. + /// + public SourceGenJsonTypeInfo( + JsonSerializerOptions options, + JsonCollectionInfoValues collectionInfo!!, + Func> converterCreator, + object? createObjectWithArgs = null, + object? addFunc = null) + : base(typeof(T), options) + { + ConverterStrategy strategy = collectionInfo.KeyInfo == null ? ConverterStrategy.Enumerable : ConverterStrategy.Dictionary; + JsonConverter converter = new JsonMetadataServicesConverter(converterCreator, strategy); + + KeyType = converter.KeyType; + ElementType = converter.ElementType; + KeyTypeInfo = collectionInfo.KeyInfo; + ElementTypeInfo = collectionInfo.ElementInfo ?? throw new ArgumentNullException(nameof(collectionInfo.ElementInfo)); + NumberHandling = collectionInfo.NumberHandling; + PropertyInfoForTypeInfo = JsonMetadataServices.CreateJsonPropertyInfoForClassInfo(typeof(T), this, converter, options); + SerializeHandler = collectionInfo.SerializeHandler; + CreateObjectWithArgs = createObjectWithArgs; + AddMethodDelegate = addFunc; + SetCreateObjectFunc(collectionInfo.ObjectCreator); + } + + internal override void LateAddProperties() + { + AddPropertiesUsingSourceGenInfo(); + } + + internal override JsonParameterInfoValues[] GetParameterInfoValues() + { + JsonSerializerContext? context = Options.JsonSerializerContext; + JsonParameterInfoValues[] array; + if (context == null || CtorParamInitFunc == null || (array = CtorParamInitFunc()) == null) + { + ThrowHelper.ThrowInvalidOperationException_NoMetadataForTypeCtorParams(context, Type); + return null!; + } + + return array; + } + + internal void AddPropertiesUsingSourceGenInfo() + { + if (PropertyInfoForTypeInfo.ConverterStrategy != ConverterStrategy.Object) + { + return; + } + + JsonSerializerContext? context = Options.JsonSerializerContext; + JsonPropertyInfo[] array; + if (context == null || PropInitFunc == null || (array = PropInitFunc(context)) == null) + { + if (typeof(T) == typeof(object)) + { + return; + } + + if (PropertyInfoForTypeInfo.ConverterBase.ElementType != null) + { + // Nullable<> or F# optional converter's strategy is set to element's strategy + return; + } + + if (SerializeHandler != null && Options.JsonSerializerContext?.CanUseSerializationLogic == true) + { + ThrowOnDeserialize = true; + return; + } + + ThrowHelper.ThrowInvalidOperationException_NoMetadataForTypeProperties(context, Type); + return; + } + + Dictionary? ignoredMembers = null; + JsonPropertyDictionary propertyCache = new(Options.PropertyNameCaseInsensitive, array.Length); + + for (int i = 0; i < array.Length; i++) + { + JsonPropertyInfo jsonPropertyInfo = array[i]; + bool hasJsonInclude = jsonPropertyInfo.SrcGen_HasJsonInclude; + + if (!jsonPropertyInfo.SrcGen_IsPublic) + { + if (hasJsonInclude) + { + ThrowHelper.ThrowInvalidOperationException_JsonIncludeOnNonPublicInvalid(jsonPropertyInfo.ClrName!, jsonPropertyInfo.DeclaringType); + } + + continue; + } + + if (jsonPropertyInfo.MemberType == MemberTypes.Field && !hasJsonInclude && !Options.IncludeFields) + { + continue; + } + + if (jsonPropertyInfo.SrcGen_IsExtensionData) + { + // Source generator compile-time type inspection has performed this validation for us. + // except JsonTypeInfo can be initialized in parallel causing this to be ocassionally re-initialized + // Debug.Assert(DataExtensionProperty == null); + Debug.Assert(IsValidDataExtensionProperty(jsonPropertyInfo)); + + DataExtensionProperty = jsonPropertyInfo; + continue; + } + + CacheMember(jsonPropertyInfo, propertyCache, ref ignoredMembers); + } + + PropertyCache = propertyCache; + } + + private void SetCreateObjectFunc(Func? createObjectFunc) + { + if (createObjectFunc != null) + { + CreateObject = () => createObjectFunc(); + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs index 023dd8b098036..ebc2799cdefd0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs @@ -109,7 +109,7 @@ internal void Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinuation = f SupportContinuation = supportContinuation; Current.JsonTypeInfo = jsonTypeInfo; Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo; - Current.NumberHandling = Current.JsonPropertyInfo.NumberHandling; + Current.NumberHandling = Current.JsonPropertyInfo.EffectiveNumberHandling; UseFastPath = !supportContinuation && !CanContainMetadata; } @@ -138,7 +138,7 @@ public void Push() Current.JsonTypeInfo = jsonTypeInfo; Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo; // Allow number handling on property to win over handling on type. - Current.NumberHandling = numberHandling ?? Current.JsonPropertyInfo.NumberHandling; + Current.NumberHandling = numberHandling ?? Current.JsonPropertyInfo.EffectiveNumberHandling; } } else @@ -346,7 +346,7 @@ public JsonTypeInfo GetTopJsonTypeInfoWithParameterizedConstructor() [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetConstructorArgumentState() { - if (Current.JsonTypeInfo.IsObjectWithParameterizedCtor) + if (Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase.ConstructorIsParameterized) { // A zero index indicates a new stack frame. if (Current.CtorArgumentStateIndex == 0) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs index 7f085ac13fa10..83ba7d51aa5fa 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs @@ -115,7 +115,7 @@ internal JsonConverter Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinu Current.JsonTypeInfo = jsonTypeInfo; Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo; - Current.NumberHandling = Current.JsonPropertyInfo.NumberHandling; + Current.NumberHandling = Current.JsonPropertyInfo.EffectiveNumberHandling; JsonSerializerOptions options = jsonTypeInfo.Options; if (options.ReferenceHandlingStrategy != ReferenceHandlingStrategy.None) @@ -154,7 +154,7 @@ public void Push() Current.JsonTypeInfo = jsonTypeInfo; Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo; // Allow number handling on property to win over handling on type. - Current.NumberHandling = numberHandling ?? Current.JsonPropertyInfo.NumberHandling; + Current.NumberHandling = numberHandling ?? Current.JsonPropertyInfo.EffectiveNumberHandling; } } else diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index b34e6a90d8534..51af633d11381 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -599,14 +599,14 @@ public static void ThrowInvalidOperationException_MetadatInitFuncsNull() throw new InvalidOperationException(SR.Format(SR.MetadataInitFuncsNull)); } - public static void ThrowInvalidOperationException_NoMetadataForTypeProperties(JsonSerializerContext context, Type type) + public static void ThrowInvalidOperationException_NoMetadataForTypeProperties(JsonSerializerContext? context, Type type) { - throw new InvalidOperationException(SR.Format(SR.NoMetadataForTypeProperties, context.GetType(), type)); + throw new InvalidOperationException(SR.Format(SR.NoMetadataForTypeProperties, context?.GetType().FullName ?? "", type)); } - public static void ThrowInvalidOperationException_NoMetadataForTypeCtorParams(JsonSerializerContext context, Type type) + public static void ThrowInvalidOperationException_NoMetadataForTypeCtorParams(JsonSerializerContext? context, Type type) { - throw new InvalidOperationException(SR.Format(SR.NoMetadataForTypeCtorParams, context.GetType(), type)); + throw new InvalidOperationException(SR.Format(SR.NoMetadataForTypeCtorParams, context?.GetType().FullName ?? "", type)); } public static void ThrowInvalidOperationException_NoDefaultOptionsForContext(JsonSerializerContext context, Type type) diff --git a/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.IgnoreCycles.cs b/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.IgnoreCycles.cs index 40eb976705251..3a692ee98b562 100644 --- a/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.IgnoreCycles.cs +++ b/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.IgnoreCycles.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.IO; using System.Threading.Tasks; using Xunit; diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs index 1a1dd3a3906dd..202a014f2d607 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/MixedModeContextTests.cs @@ -138,9 +138,11 @@ public override void RoundTripEmptyPoco() EmptyPoco expected = CreateEmptyPoco(); string json = JsonSerializer.Serialize(expected, DefaultContext.EmptyPoco); - JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.EmptyPoco), typeof(EmptyPoco)); + // This would have thrown if we tried to lookup any properties but since there are no properties this is able to complete. + EmptyPoco obj = JsonSerializer.Deserialize(json, DefaultContext.EmptyPoco); + VerifyEmptyPoco(expected, obj); - EmptyPoco obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataWithPerTypeAttributeContext.Default).EmptyPoco); + obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataWithPerTypeAttributeContext.Default).EmptyPoco); VerifyEmptyPoco(expected, obj); AssertFastPathLogicCorrect(json, obj, DefaultContext.EmptyPoco); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs index a71e76adbe2cf..cfc6d486c7dd0 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/RealWorldContextTests.cs @@ -127,7 +127,16 @@ public virtual void RoundTripValueTuple() if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization) { // Deserialization not supported in fast path serialization only mode - Assert.Throws(() => JsonSerializer.Deserialize(json, DefaultContext.ValueTupleStringInt32Boolean)); + // but if there are no fields we won't throw because we throw on the property lookup + if (isIncludeFieldsEnabled) + { + Assert.Throws(() => JsonSerializer.Deserialize(json, DefaultContext.ValueTupleStringInt32Boolean)); + } + else + { + (string, int, bool) obj = JsonSerializer.Deserialize(json, DefaultContext.ValueTupleStringInt32Boolean); + Assert.Equal(default((string, int, bool)), obj); + } } else { @@ -910,16 +919,11 @@ public void TypeWithDerivedAttribute() string json = JsonSerializer.Serialize(instance, DefaultContext.TypeWithDerivedAttribute); JsonTestHelper.AssertJsonEqual(@"{}", json); - if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization) - { - // Deserialization not supported in fast path serialization only mode - Assert.Throws(() => JsonSerializer.Deserialize(json, DefaultContext.TypeWithDerivedAttribute)); - } - else - { - instance = JsonSerializer.Deserialize(json, DefaultContext.TypeWithDerivedAttribute); - Assert.NotNull(instance); - } + + // Deserialization not supported in fast path serialization only mode + // but we can deserialize empty types as we throw only when looking up properties and there are no properties here. + instance = JsonSerializer.Deserialize(json, DefaultContext.TypeWithDerivedAttribute); + Assert.NotNull(instance); } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs index ebed41a5ad3c5..560734c8a1e24 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs @@ -27,14 +27,20 @@ protected PropertyVisibilityTests_Metadata(Serialization.Tests.JsonSerializerWra [InlineData(typeof(StructWithBadIgnoreAttribute))] public override async Task JsonIgnoreCondition_WhenWritingNull_OnValueType_Fail_EmptyJson(Type type) { - await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper("", type)); - - InvalidOperationException ioe = await Assert.ThrowsAsync(async () => await Serializer.SerializeWrapper(Activator.CreateInstance(type), type)); - string exAsStr = ioe.ToString(); - Assert.Contains("JsonIgnoreCondition.WhenWritingNull", exAsStr); - Assert.Contains("MyBadMember", exAsStr); - Assert.Contains(type.ToString(), exAsStr); - Assert.Contains("JsonIgnoreCondition.WhenWritingDefault", exAsStr); + InvalidOperationException ioe = await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper("", type)); + ValidateInvalidOperationException(); + + ioe = await Assert.ThrowsAsync(async () => await Serializer.SerializeWrapper(Activator.CreateInstance(type), type)); + ValidateInvalidOperationException(); + + void ValidateInvalidOperationException() + { + string exAsStr = ioe.ToString(); + Assert.Contains("JsonIgnoreCondition.WhenWritingNull", exAsStr); + Assert.Contains("MyBadMember", exAsStr); + Assert.Contains(type.ToString(), exAsStr); + Assert.Contains("JsonIgnoreCondition.WhenWritingDefault", exAsStr); + } } [Fact] @@ -314,7 +320,7 @@ public override async Task JsonIgnoreCondition_WhenWritingNull_OnValueType_Fail( public override async Task JsonIgnoreCondition_WhenWritingNull_OnValueType_Fail_EmptyJson(Type type) { // Since this code goes down fast-path, there's no warm up and we hit the reader exception about having no tokens. - await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper("", type)); + await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper("", type)); await Assert.ThrowsAsync(async () => await Serializer.SerializeWrapper(Activator.CreateInstance(type), type)); } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs index 765f8a883454d..b177cfff8ea11 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/SerializationContextTests.cs @@ -269,9 +269,11 @@ public override void RoundTripEmptyPoco() EmptyPoco expected = CreateEmptyPoco(); string json = JsonSerializer.Serialize(expected, DefaultContext.EmptyPoco); - JsonTestHelper.AssertThrows_PropMetadataInit(() => JsonSerializer.Deserialize(json, DefaultContext.EmptyPoco), typeof(EmptyPoco)); + // This would have thrown on the first property lookup but JSON is empty here + EmptyPoco obj = JsonSerializer.Deserialize(json, DefaultContext.EmptyPoco); + VerifyEmptyPoco(expected, obj); - EmptyPoco obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataWithPerTypeAttributeContext.Default).EmptyPoco); + obj = JsonSerializer.Deserialize(json, ((ITestContext)MetadataWithPerTypeAttributeContext.Default).EmptyPoco); VerifyEmptyPoco(expected, obj); AssertFastPathLogicCorrect(json, obj, DefaultContext.EmptyPoco); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerApiValidation.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerApiValidation.cs index ff1c6db60e23e..1057e6a89006c 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerApiValidation.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerApiValidation.cs @@ -81,10 +81,14 @@ private static JsonTypeInfo GetTypeInfo() JsonObjectInfoValues objectInfo = new() { ObjectCreator = static () => throw new NotImplementedException(), - SerializeHandler = (Utf8JsonWriter writer, MyPoco value) => throw new NotImplementedException() + SerializeHandler = (Utf8JsonWriter writer, MyPoco value) => throw new NotImplementedException(), + PropertyMetadataInitializer = (ctx) => new JsonPropertyInfo[0], }; - return JsonMetadataServices.CreateObjectInfo(new JsonSerializerOptions(), objectInfo); + var options = new JsonSerializerOptions(); + options.AddContext(); + + return JsonMetadataServices.CreateObjectInfo(options, objectInfo); } [Fact] From 08ca87b763c0637356bca03a8ba295b590234350 Mon Sep 17 00:00:00 2001 From: Krzysztof Wicher Date: Tue, 5 Apr 2022 11:00:39 +0200 Subject: [PATCH 2/3] Apply PR feedback --- .../Json/Serialization/JsonSerializerOptions.Converters.cs | 3 ++- .../System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs | 2 +- .../System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs | 2 +- .../Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) 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 846d8908d5cc3..95da81f6c2970 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 @@ -46,6 +46,8 @@ static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options) JsonTypeInfo.ValidateType(type, null, null, options); MethodInfo methodInfo = typeof(JsonSerializerOptions).GetMethod(nameof(CreateReflectionJsonTypeInfo), BindingFlags.NonPublic | BindingFlags.Instance)!; + // Some of the validation is done during construction (i.e. validity of JsonConverter, inner types etc.) + // therefore we need to unwrap TargetInvocationException for better user experience #if NETCOREAPP return (JsonTypeInfo)methodInfo.MakeGenericMethod(type).Invoke(options, BindingFlags.NonPublic | BindingFlags.DoNotWrapExceptions, null, null, null)!; #else @@ -64,7 +66,6 @@ static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options) [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] private JsonTypeInfo CreateReflectionJsonTypeInfo() { - // We do not use Activator.CreateInstance because it will wrap exception if constructor throws it return new ReflectionJsonTypeInfo(this); } 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 275343b55a1b5..baf429066e87c 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 @@ -167,7 +167,7 @@ internal JsonTypeInfo() Debug.Assert(false, "This constructor should not be called."); } - internal JsonTypeInfo(Type type, JsonSerializerOptions options!!, bool dummy) + internal JsonTypeInfo(Type type, JsonSerializerOptions options!!) { Type = type; Options = options; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs index fec8cbe6bc40b..6e4a5a40b4815 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs @@ -16,7 +16,7 @@ public abstract class JsonTypeInfo : JsonTypeInfo private Action? _serialize; internal JsonTypeInfo(Type type, JsonSerializerOptions options) : - base(type, options, dummy: false) + base(type, options) { } internal JsonTypeInfo(JsonConverter converter, JsonSerializerOptions options) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs index 5db3e52a89e83..ee1df1392d5b2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionJsonTypeInfoOfT.cs @@ -13,7 +13,7 @@ namespace System.Text.Json.Serialization.Metadata /// /// Provides JSON serialization-related metadata about a type. /// - internal class ReflectionJsonTypeInfo : JsonTypeInfo + internal sealed class ReflectionJsonTypeInfo : JsonTypeInfo { [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] internal ReflectionJsonTypeInfo(JsonSerializerOptions options) From a772f78eba560b9b640cad77c30f735dccf1ebcc Mon Sep 17 00:00:00 2001 From: Krzysztof Wicher Date: Wed, 6 Apr 2022 15:25:19 +0200 Subject: [PATCH 3/3] improve exception unwrapping --- .../Serialization/JsonSerializerOptions.Converters.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 95da81f6c2970..4c977084c51d1 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 @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.ExceptionServices; using System.Text.Json.Reflection; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Converters; @@ -46,8 +47,6 @@ static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options) JsonTypeInfo.ValidateType(type, null, null, options); MethodInfo methodInfo = typeof(JsonSerializerOptions).GetMethod(nameof(CreateReflectionJsonTypeInfo), BindingFlags.NonPublic | BindingFlags.Instance)!; - // Some of the validation is done during construction (i.e. validity of JsonConverter, inner types etc.) - // therefore we need to unwrap TargetInvocationException for better user experience #if NETCOREAPP return (JsonTypeInfo)methodInfo.MakeGenericMethod(type).Invoke(options, BindingFlags.NonPublic | BindingFlags.DoNotWrapExceptions, null, null, null)!; #else @@ -57,7 +56,10 @@ static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options) } catch (TargetInvocationException ex) { - throw ex.InnerException!; + // Some of the validation is done during construction (i.e. validity of JsonConverter, inner types etc.) + // therefore we need to unwrap TargetInvocationException for better user experience + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + throw ex.InnerException; } #endif }