Refactor JsonSerializer metadata reading infrastructure (#67183)
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Fri, 1 Apr 2022 09:47:46 +0000 (10:47 +0100)
committerGitHub <noreply@github.com>
Fri, 1 Apr 2022 09:47:46 +0000 (10:47 +0100)
* Refactor metadata reading infrastructure.

* Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs

27 files changed:
src/libraries/System.Text.Json/src/Resources/Strings.resx
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableEnumerableOfTConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/MetadataPropertyName.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/StackFrameObjectState.cs
src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.Exceptions.cs
src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.Deserialize.cs
src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ReferenceHandlerTests.cs

index 100a206..967d2d0 100644 (file)
   <data name="ConstructorMaxOf64Parameters" xml:space="preserve">
     <value>The deserialization constructor on type '{0}' may not have more than 64 parameters for deserialization.</value>
   </data>
-  <data name="ObjectWithParameterizedCtorRefMetadataNotHonored" xml:space="preserve">
-    <value>Reference metadata is not honored when deserializing types using parameterized constructors. See type '{0}'.</value>
+  <data name="ObjectWithParameterizedCtorRefMetadataNotSupported" xml:space="preserve">
+    <value>Reference metadata is not supported when deserializing constructor parameters. See type '{0}'.</value>
   </data>
   <data name="SerializerConverterFactoryReturnsNull" xml:space="preserve">
     <value>The converter '{0}' cannot return a null value.</value>
index 3ae0e3e..aff697b 100644 (file)
@@ -11,7 +11,7 @@ namespace System.Text.Json.Serialization.Converters
     /// </summary>
     internal sealed class ArrayConverter<TCollection, TElement> : IEnumerableDefaultConverter<TElement[], TElement>
     {
-        internal override bool CanHaveIdMetadata => false;
+        internal override bool CanHaveMetadata => false;
 
         protected override void Add(in TElement value, ref ReadStack state)
         {
index 330444d..e53a6fb 100644 (file)
@@ -16,7 +16,7 @@ namespace System.Text.Json.Serialization.Converters
         where TDictionary : IEnumerable<KeyValuePair<TKey, TValue>>
         where TKey : notnull
     {
-        internal override bool CanHaveIdMetadata => true;
+        internal override bool CanHaveMetadata => true;
 
         protected internal override bool OnWriteResume(
             Utf8JsonWriter writer,
index c8d1c5f..f107910 100644 (file)
@@ -12,7 +12,7 @@ namespace System.Text.Json.Serialization.Converters
     internal abstract class IEnumerableDefaultConverter<TCollection, TElement> : JsonCollectionConverter<TCollection, TElement>
         where TCollection : IEnumerable<TElement>
     {
-        internal override bool CanHaveIdMetadata => true;
+        internal override bool CanHaveMetadata => true;
 
         protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state)
         {
index 78059a1..98d2479 100644 (file)
@@ -17,7 +17,7 @@ namespace System.Text.Json.Serialization.Converters
             ((Dictionary<TKey, TValue>)state.Current.ReturnValue!)[key] = value;
         }
 
-        internal sealed override bool CanHaveIdMetadata => false;
+        internal sealed override bool CanHaveMetadata => false;
 
         protected sealed override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state)
         {
index 801b78b..fc7db3e 100644 (file)
@@ -16,7 +16,7 @@ namespace System.Text.Json.Serialization.Converters
             ((List<TElement>)state.Current.ReturnValue!).Add(value);
         }
 
-        internal sealed override bool CanHaveIdMetadata => false;
+        internal sealed override bool CanHaveMetadata => false;
 
         protected sealed override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options)
         {
index 63568ee..b5aeca4 100644 (file)
@@ -120,16 +120,15 @@ namespace System.Text.Json.Serialization
             }
             else
             {
-                // Slower path that supports continuation and preserved references.
+                // Slower path that supports continuation and reading metadata.
 
-                bool preserveReferences = options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve;
                 if (state.Current.ObjectState == StackFrameObjectState.None)
                 {
                     if (reader.TokenType == JsonTokenType.StartArray)
                     {
-                        state.Current.ObjectState = StackFrameObjectState.PropertyValue;
+                        state.Current.ObjectState = StackFrameObjectState.ReadMetadata;
                     }
-                    else if (preserveReferences)
+                    else if (state.CanContainMetadata)
                     {
                         if (reader.TokenType != JsonTokenType.StartObject)
                         {
@@ -147,27 +146,41 @@ namespace System.Text.Json.Serialization
                 }
 
                 // Handle the metadata properties.
-                if (preserveReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue)
+                if (state.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata)
                 {
-                    if (JsonSerializer.ResolveMetadataForJsonArray<TCollection>(ref reader, ref state, options))
-                    {
-                        if (state.Current.ObjectState == StackFrameObjectState.ReadRefEndObject)
-                        {
-                            // This will never throw since it was previously validated in ResolveMetadataForJsonArray.
-                            value = (TCollection)state.Current.ReturnValue!;
-                            return true;
-                        }
-                    }
-                    else
+                    if (!JsonSerializer.TryReadMetadata(this, ref reader, ref state))
                     {
                         value = default;
                         return false;
                     }
+
+                    state.Current.ObjectState = StackFrameObjectState.ReadMetadata;
                 }
 
                 if (state.Current.ObjectState < StackFrameObjectState.CreatedObject)
                 {
+                    if (state.CanContainMetadata)
+                    {
+                        JsonSerializer.ValidateMetadataForArrayConverter(this, ref reader, ref state);
+                    }
+
+                    if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref)
+                    {
+                        value = JsonSerializer.ResolveReferenceId<TCollection>(ref state);
+                        return true;
+                    }
+
                     CreateCollection(ref reader, ref state, options);
+
+                    if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Id))
+                    {
+                        Debug.Assert(state.ReferenceId != null);
+                        Debug.Assert(options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve);
+                        Debug.Assert(state.Current.ReturnValue is TCollection);
+                        state.ReferenceResolver.AddReference(state.ReferenceId, state.Current.ReturnValue);
+                        state.ReferenceId = null;
+                    }
+
                     state.Current.ObjectState = StackFrameObjectState.CreatedObject;
                 }
 
@@ -222,8 +235,8 @@ namespace System.Text.Json.Serialization
                 {
                     state.Current.ObjectState = StackFrameObjectState.EndToken;
 
-                    // Read the EndObject token for an array with preserve semantics.
-                    if (state.Current.ValidateEndTokenOnArray)
+                    // Array payload is nested inside a $values metadata property.
+                    if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Values))
                     {
                         if (!reader.Read())
                         {
@@ -235,7 +248,8 @@ namespace System.Text.Json.Serialization
 
                 if (state.Current.ObjectState < StackFrameObjectState.EndTokenValidation)
                 {
-                    if (state.Current.ValidateEndTokenOnArray)
+                    // Array payload is nested inside a $values metadata property.
+                    if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Values))
                     {
                         if (reader.TokenType != JsonTokenType.EndObject)
                         {
@@ -304,8 +318,5 @@ namespace System.Text.Json.Serialization
         }
 
         protected abstract bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state);
-
-        internal sealed override void CreateInstanceForReferenceResolver(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options)
-            => CreateCollection(ref reader, ref state, options);
     }
 }
index 9f2e346..c4ef113 100644 (file)
@@ -155,7 +155,7 @@ namespace System.Text.Json.Serialization
             }
             else
             {
-                // Slower path that supports continuation and preserved references.
+                // Slower path that supports continuation and reading metadata.
 
                 if (state.Current.ObjectState == StackFrameObjectState.None)
                 {
@@ -168,29 +168,42 @@ namespace System.Text.Json.Serialization
                 }
 
                 // Handle the metadata properties.
-                bool preserveReferences = options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve;
-                if (preserveReferences && state.Current.ObjectState < StackFrameObjectState.PropertyValue)
+                if (state.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata)
                 {
-                    if (JsonSerializer.ResolveMetadataForJsonObject<TDictionary>(ref reader, ref state, options))
-                    {
-                        if (state.Current.ObjectState == StackFrameObjectState.ReadRefEndObject)
-                        {
-                            // This will never throw since it was previously validated in ResolveMetadataForJsonObject.
-                            value = (TDictionary)state.Current.ReturnValue!;
-                            return true;
-                        }
-                    }
-                    else
+                    if (!JsonSerializer.TryReadMetadata(this, ref reader, ref state))
                     {
                         value = default;
                         return false;
                     }
+
+                    state.Current.ObjectState = StackFrameObjectState.ReadMetadata;
                 }
 
                 // Create the dictionary.
                 if (state.Current.ObjectState < StackFrameObjectState.CreatedObject)
                 {
+                    if (state.CanContainMetadata)
+                    {
+                        JsonSerializer.ValidateMetadataForObjectConverter(this, ref reader, ref state);
+                    }
+
+                    if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref)
+                    {
+                        value = JsonSerializer.ResolveReferenceId<TDictionary>(ref state);
+                        return true;
+                    }
+
                     CreateCollection(ref reader, ref state);
+
+                    if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Id))
+                    {
+                        Debug.Assert(state.ReferenceId != null);
+                        Debug.Assert(options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve);
+                        Debug.Assert(state.Current.ReturnValue is TDictionary);
+                        state.ReferenceResolver.AddReference(state.ReferenceId, state.Current.ReturnValue);
+                        state.ReferenceId = null;
+                    }
+
                     state.Current.ObjectState = StackFrameObjectState.CreatedObject;
                 }
 
@@ -225,7 +238,7 @@ namespace System.Text.Json.Serialization
 
                         state.Current.PropertyState = StackFramePropertyState.Name;
 
-                        if (preserveReferences)
+                        if (state.CanContainMetadata)
                         {
                             ReadOnlySpan<byte> propertyName = reader.GetSpan();
                             if (propertyName.Length > 0 && propertyName[0] == '$')
@@ -334,8 +347,5 @@ namespace System.Text.Json.Serialization
 
             return success;
         }
-
-        internal sealed override void CreateInstanceForReferenceResolver(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options)
-            => CreateCollection(ref reader, ref state);
     }
 }
index 88d89f6..ba3b0b7 100644 (file)
@@ -25,7 +25,7 @@ namespace System.Text.Json.Serialization.Converters
             ((List<Tuple<TKey, TValue>>)state.Current.ReturnValue!).Add (new Tuple<TKey, TValue>(key, value));
         }
 
-        internal override bool CanHaveIdMetadata => false;
+        internal override bool CanHaveMetadata => false;
 
         protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state)
         {
index 71e3b73..9a29b32 100644 (file)
@@ -41,7 +41,7 @@ namespace System.Text.Json.Serialization.Converters
 
         internal override bool ConstructorIsParameterized => Converter.ConstructorIsParameterized;
 
-        internal override bool CanHaveIdMetadata => Converter.CanHaveIdMetadata;
+        internal override bool CanHaveMetadata => Converter.CanHaveMetadata;
 
         public JsonMetadataServicesConverter(Func<JsonConverter<T>> converterCreator!!, ConverterStrategy converterStrategy)
         {
@@ -94,8 +94,5 @@ namespace System.Text.Json.Serialization.Converters
 
         internal override void ConfigureJsonTypeInfo(JsonTypeInfo jsonTypeInfo, JsonSerializerOptions options)
             => Converter.ConfigureJsonTypeInfo(jsonTypeInfo, options);
-
-        internal override void CreateInstanceForReferenceResolver(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options)
-            => Converter.CreateInstanceForReferenceResolver(ref reader, ref state, options);
     }
 }
index 90d413d..6c0a095 100644 (file)
@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Diagnostics;
+using System.Text.Json.Nodes;
 
 namespace System.Text.Json.Serialization.Converters
 {
@@ -36,13 +37,15 @@ namespace System.Text.Json.Serialization.Converters
 
         internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out object? value)
         {
+            object? referenceValue;
+
             if (options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonElement)
             {
                 JsonElement element = JsonElement.ParseValue(ref reader);
 
                 // Edge case where we want to lookup for a reference when parsing into typeof(object)
                 if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve &&
-                    JsonSerializer.TryGetReferenceFromJsonElement(ref state, element, out object? referenceValue))
+                    JsonSerializer.TryHandleReferenceFromJsonElement(ref reader, ref state, element, out referenceValue))
                 {
                     value = referenceValue;
                 }
@@ -55,8 +58,19 @@ namespace System.Text.Json.Serialization.Converters
             }
 
             Debug.Assert(options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonNode);
-            value = JsonNodeConverter.Instance.Read(ref reader, typeToConvert, options)!;
-            // TODO reference lookup for JsonNode deserialization.
+
+            JsonNode node = JsonNodeConverter.Instance.Read(ref reader, typeToConvert, options)!;
+
+            if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve &&
+                JsonSerializer.TryHandleReferenceFromJsonNode(ref reader, ref state, node, out referenceValue))
+            {
+                value = referenceValue;
+            }
+            else
+            {
+                value = node;
+            }
+
             return true;
         }
 
index e4f139b..6894c4a 100644 (file)
@@ -14,7 +14,7 @@ namespace System.Text.Json.Serialization.Converters
     /// </summary>
     internal class ObjectDefaultConverter<T> : JsonObjectConverter<T> where T : notnull
     {
-        internal override bool CanHaveIdMetadata => true;
+        internal override bool CanHaveMetadata => true;
 
         internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, [MaybeNullWhen(false)] out T value)
         {
@@ -72,7 +72,7 @@ namespace System.Text.Json.Serialization.Converters
             }
             else
             {
-                // Slower path that supports continuation and preserved references.
+                // Slower path that supports continuation and reading metadata.
 
                 if (state.Current.ObjectState == StackFrameObjectState.None)
                 {
@@ -85,29 +85,30 @@ namespace System.Text.Json.Serialization.Converters
                 }
 
                 // Handle the metadata properties.
-                if (state.Current.ObjectState < StackFrameObjectState.PropertyValue)
+                if (state.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata)
                 {
-                    if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve)
+                    if (!JsonSerializer.TryReadMetadata(this, ref reader, ref state))
                     {
-                        if (JsonSerializer.ResolveMetadataForJsonObject<T>(ref reader, ref state, options))
-                        {
-                            if (state.Current.ObjectState == StackFrameObjectState.ReadRefEndObject)
-                            {
-                                // This will never throw since it was previously validated in ResolveMetadataForJsonObject.
-                                value = (T)state.Current.ReturnValue!;
-                                return true;
-                            }
-                        }
-                        else
-                        {
-                            value = default;
-                            return false;
-                        }
+                        value = default;
+                        return false;
                     }
+
+                    state.Current.ObjectState = StackFrameObjectState.ReadMetadata;
                 }
 
                 if (state.Current.ObjectState < StackFrameObjectState.CreatedObject)
                 {
+                    if (state.CanContainMetadata)
+                    {
+                        JsonSerializer.ValidateMetadataForObjectConverter(this, ref reader, ref state);
+                    }
+
+                    if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref)
+                    {
+                        value = JsonSerializer.ResolveReferenceId<T>(ref state);
+                        return true;
+                    }
+
                     if (jsonTypeInfo.CreateObject == null)
                     {
                         ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(jsonTypeInfo.Type, ref reader, ref state);
@@ -115,6 +116,14 @@ namespace System.Text.Json.Serialization.Converters
 
                     obj = jsonTypeInfo.CreateObject!()!;
 
+                    if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Id))
+                    {
+                        Debug.Assert(state.ReferenceId != null);
+                        Debug.Assert(options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve);
+                        state.ReferenceResolver.AddReference(state.ReferenceId, obj);
+                        state.ReferenceId = null;
+                    }
+
                     if (obj is IJsonOnDeserializing onDeserializing)
                     {
                         onDeserializing.OnDeserializing();
@@ -453,21 +462,5 @@ namespace System.Text.Json.Serialization.Converters
 
             return true;
         }
-
-        internal sealed override void CreateInstanceForReferenceResolver(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options)
-        {
-            if (state.Current.JsonTypeInfo.CreateObject == null)
-            {
-                ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(state.Current.JsonTypeInfo.Type, ref reader, ref state);
-            }
-
-            object obj = state.Current.JsonTypeInfo.CreateObject!()!;
-            state.Current.ReturnValue = obj;
-
-            if (obj is IJsonOnDeserializing onDeserializing)
-            {
-                onDeserializing.OnDeserializing();
-            }
-        }
     }
 }
index 661d224..35848ce 100644 (file)
@@ -30,6 +30,11 @@ namespace System.Text.Json.Serialization.Converters
             {
                 // Fast path that avoids maintaining state variables.
 
+                if (reader.TokenType != JsonTokenType.StartObject)
+                {
+                    ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert);
+                }
+
                 ReadOnlySpan<byte> originalSpan = reader.OriginalSpan;
 
                 ReadConstructorArguments(ref state, ref reader, options);
@@ -85,12 +90,46 @@ namespace System.Text.Json.Serialization.Converters
             }
             else
             {
-                // Slower path that supports continuation.
+                // Slower path that supports continuation and metadata reads.
 
                 if (state.Current.ObjectState == StackFrameObjectState.None)
                 {
+                    if (reader.TokenType != JsonTokenType.StartObject)
+                    {
+                        ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert);
+                    }
+
                     state.Current.ObjectState = StackFrameObjectState.StartToken;
-                    BeginRead(ref state, ref reader,  options);
+                }
+
+                // Read any metadata properties.
+                if (state.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata)
+                {
+                    if (!JsonSerializer.TryReadMetadata(this, ref reader, ref state))
+                    {
+                        value = default;
+                        return false;
+                    }
+
+                    state.Current.ObjectState = StackFrameObjectState.ReadMetadata;
+                }
+
+                if (state.Current.ObjectState < StackFrameObjectState.ConstructorArguments)
+                {
+                    if (state.CanContainMetadata)
+                    {
+                        JsonSerializer.ValidateMetadataForObjectConverter(this, ref reader, ref state);
+                    }
+
+                    if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref)
+                    {
+                        value = JsonSerializer.ResolveReferenceId<T>(ref state);
+                        return true;
+                    }
+
+                    BeginRead(ref state, ref reader, options);
+
+                    state.Current.ObjectState = StackFrameObjectState.ConstructorArguments;
                 }
 
                 if (!ReadConstructorArgumentsWithContinuation(ref state, ref reader, options))
@@ -101,6 +140,14 @@ namespace System.Text.Json.Serialization.Converters
 
                 obj = (T)CreateObject(ref state.Current);
 
+                if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Id))
+                {
+                    Debug.Assert(state.ReferenceId != null);
+                    Debug.Assert(options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve);
+                    state.ReferenceResolver.AddReference(state.ReferenceId, obj);
+                    state.ReferenceId = null;
+                }
+
                 if (obj is IJsonOnDeserializing onDeserializing)
                 {
                     onDeserializing.OnDeserializing();
@@ -458,11 +505,6 @@ namespace System.Text.Json.Serialization.Converters
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         private void BeginRead(ref ReadStack state, ref Utf8JsonReader reader, JsonSerializerOptions options)
         {
-            if (reader.TokenType != JsonTokenType.StartObject)
-            {
-                ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(TypeToConvert);
-            }
-
             if (state.Current.JsonTypeInfo.ParameterCount != state.Current.JsonTypeInfo.ParameterCache!.Count)
             {
                 ThrowHelper.ThrowInvalidOperationException_ConstructorParameterIncompleteBinding(TypeToConvert);
index 7a83e19..a02ca76 100644 (file)
@@ -31,7 +31,7 @@ namespace System.Text.Json.Serialization
         /// <summary>
         /// Can the converter have $id metadata.
         /// </summary>
-        internal virtual bool CanHaveIdMetadata => false;
+        internal virtual bool CanHaveMetadata => false;
 
         /// <summary>
         /// The converter supports polymorphic writes; only reserved for System.Object types.
@@ -132,10 +132,5 @@ namespace System.Text.Json.Serialization
         /// </summary>
         [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
         internal virtual void ConfigureJsonTypeInfoUsingReflection(JsonTypeInfo jsonTypeInfo, JsonSerializerOptions options) { }
-
-        /// <summary>
-        /// Creates the instance and assigns it to state.Current.ReturnValue.
-        /// </summary>
-        internal virtual void CreateInstanceForReferenceResolver(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options) { }
     }
 }
index a29b187..f7f6d2a 100644 (file)
@@ -337,8 +337,8 @@ namespace System.Text.Json.Serialization
                             break;
 
                         case ReferenceHandlingStrategy.Preserve:
-                            bool canHaveIdMetata = polymorphicConverter?.CanHaveIdMetadata ?? CanHaveIdMetadata;
-                            if (canHaveIdMetata && JsonSerializer.TryGetReferenceForValue(value, ref state, writer))
+                            bool canHaveMetadata = polymorphicConverter?.CanHaveMetadata ?? CanHaveMetadata;
+                            if (canHaveMetadata && JsonSerializer.TryGetReferenceForValue(value, ref state, writer))
                             {
                                 // We found a repeating reference and wrote the relevant metadata; serialization complete.
                                 return true;
index e61b52c..b262cdd 100644 (file)
@@ -3,7 +3,7 @@
 
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
+using System.Text.Json.Nodes;
 using System.Text.Json.Serialization;
 
 namespace System.Text.Json
@@ -19,348 +19,161 @@ namespace System.Text.Json
         internal static readonly byte[] s_valuesPropertyName
             = new byte[] { (byte)'$', (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', (byte)'s' };
 
-        /// <summary>
-        /// Returns true if successful, false is the reader ran out of buffer.
-        /// Sets state.Current.ReturnValue to the reference target for $ref cases;
-        /// Sets state.Current.ReturnValue to a new instance for $id cases.
-        /// </summary>
-        internal static bool ResolveMetadataForJsonObject<T>(
-            ref Utf8JsonReader reader,
-            ref ReadStack state,
-            JsonSerializerOptions options)
+        internal static bool TryReadMetadata(JsonConverter converter, ref Utf8JsonReader reader, ref ReadStack state)
         {
-            JsonConverter converter = state.Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase;
-
-            if (state.Current.ObjectState < StackFrameObjectState.ReadAheadNameOrEndObject)
-            {
-                // Read the first metadata property name.
-                if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadNameOrEndObject))
-                {
-                    return false;
-                }
-            }
+            Debug.Assert(state.Current.ObjectState == StackFrameObjectState.StartToken);
+            Debug.Assert(state.CanContainMetadata);
 
-            if (state.Current.ObjectState == StackFrameObjectState.ReadNameOrEndObject)
+            while (true)
             {
-                if (reader.TokenType != JsonTokenType.PropertyName)
+                if (state.Current.PropertyState == StackFramePropertyState.None)
                 {
-                    // Since this was an empty object, we are done reading metadata.
-                    state.Current.ObjectState = StackFrameObjectState.PropertyValue;
-                    // Skip the read of the first property name, since we already read it above.
                     state.Current.PropertyState = StackFramePropertyState.ReadName;
-                    return true;
-                }
 
-                ReadOnlySpan<byte> propertyName = reader.GetSpan();
-                MetadataPropertyName metadata = GetMetadataPropertyName(propertyName);
-                if (metadata == MetadataPropertyName.Id)
-                {
-                    state.Current.JsonPropertyName = s_idPropertyName;
-
-                    if (!converter.CanHaveIdMetadata)
+                    // Read the property name.
+                    if (!reader.Read())
                     {
-                        ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(converter.TypeToConvert);
+                        return false;
                     }
-
-                    state.Current.ObjectState = StackFrameObjectState.ReadAheadIdValue;
                 }
-                else if (metadata == MetadataPropertyName.Ref)
-                {
-                    state.Current.JsonPropertyName = s_refPropertyName;
 
-                    if (converter.IsValueType)
+                if (state.Current.PropertyState < StackFramePropertyState.Name)
+                {
+                    if (reader.TokenType == JsonTokenType.EndObject)
                     {
-                        ThrowHelper.ThrowJsonException_MetadataInvalidReferenceToValueType(converter.TypeToConvert);
+                        // Read the entire object while parsing for metadata.
+                        return true;
                     }
 
-                    state.Current.ObjectState = StackFrameObjectState.ReadAheadRefValue;
-                }
-                else if (metadata == MetadataPropertyName.Values)
-                {
-                    ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(propertyName, ref state, reader);
-                }
-                else
-                {
-                    Debug.Assert(metadata == MetadataPropertyName.NoMetadata);
-                    // We are done reading metadata, the object didn't contain any.
-                    state.Current.ObjectState = StackFrameObjectState.PropertyValue;
-                    // Skip the read of the first property name, since we already read it above.
-                    state.Current.PropertyState = StackFramePropertyState.ReadName;
-                    return true;
-                }
-            }
-
-            if (state.Current.ObjectState == StackFrameObjectState.ReadAheadRefValue)
-            {
-                if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadRefValue))
-                {
-                    return false;
-                }
-            }
-            else if (state.Current.ObjectState == StackFrameObjectState.ReadAheadIdValue)
-            {
-                if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadIdValue))
-                {
-                    return false;
-                }
-            }
-
-            if (state.Current.ObjectState == StackFrameObjectState.ReadRefValue)
-            {
-                if (reader.TokenType != JsonTokenType.String)
-                {
-                    ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType);
-                }
-
-                string referenceId = reader.GetString()!;
-                object value = state.ReferenceResolver.ResolveReference(referenceId);
-                ValidateValueIsCorrectType<T>(value, referenceId);
-                state.Current.ReturnValue = value;
-
-                state.Current.ObjectState = StackFrameObjectState.ReadAheadRefEndObject;
-            }
-            else if (state.Current.ObjectState == StackFrameObjectState.ReadIdValue)
-            {
-                if (reader.TokenType != JsonTokenType.String)
-                {
-                    ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType);
-                }
-
-                converter.CreateInstanceForReferenceResolver(ref reader, ref state, options);
-
-                string referenceId = reader.GetString()!;
-                state.ReferenceResolver.AddReference(referenceId, state.Current.ReturnValue!);
-
-                // We are done reading metadata plus we instantiated the object.
-                state.Current.ObjectState = StackFrameObjectState.CreatedObject;
-            }
-
-            // Clear the metadata property name that was set in case of failure on ResolveReference/AddReference.
-            state.Current.JsonPropertyName = null;
-
-            if (state.Current.ObjectState == StackFrameObjectState.ReadAheadRefEndObject)
-            {
-                if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadRefEndObject))
-                {
-                    return false;
-                }
-            }
-
-            if (state.Current.ObjectState == StackFrameObjectState.ReadRefEndObject)
-            {
-                if (reader.TokenType != JsonTokenType.EndObject)
-                {
                     // We just read a property. The only valid next tokens are EndObject and PropertyName.
                     Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
 
-                    ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(reader.GetSpan(), ref state);
-                }
-            }
-
-            return true;
-        }
-
-        /// <summary>
-        /// Returns true if successful, false is the reader ran out of buffer.
-        /// Sets state.Current.ReturnValue to the reference target for $ref cases;
-        /// Sets state.Current.ReturnValue to a new instance for $id cases.
-        /// </summary>
-        internal static bool ResolveMetadataForJsonArray<T>(
-            ref Utf8JsonReader reader,
-            ref ReadStack state,
-            JsonSerializerOptions options)
-        {
-            JsonConverter converter = state.Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase;
-
-            if (state.Current.ObjectState < StackFrameObjectState.ReadAheadNameOrEndObject)
-            {
-                // Read the first metadata property name.
-                if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadNameOrEndObject))
-                {
-                    return false;
-                }
-            }
-
-            if (state.Current.ObjectState == StackFrameObjectState.ReadNameOrEndObject)
-            {
-                if (reader.TokenType != JsonTokenType.PropertyName)
-                {
-                    // The reader should have detected other invalid cases.
-                    Debug.Assert(reader.TokenType == JsonTokenType.EndObject);
-
-                    // An enumerable needs metadata since it starts with StartObject.
-                    ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(ref state, converter.TypeToConvert);
-                }
-
-                ReadOnlySpan<byte> propertyName = reader.GetSpan();
-                MetadataPropertyName metadata = GetMetadataPropertyName(propertyName);
-                if (metadata == MetadataPropertyName.Id)
-                {
-                    state.Current.JsonPropertyName = s_idPropertyName;
-
-                    if (!converter.CanHaveIdMetadata)
+                    if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Ref))
                     {
-                        ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(converter.TypeToConvert);
+                        // No properties whatsoever should follow a $ref property.
+                        ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(reader.GetSpan(), ref state);
                     }
 
-                    state.Current.ObjectState = StackFrameObjectState.ReadAheadIdValue;
-                }
-                else if (metadata == MetadataPropertyName.Ref)
-                {
-                    state.Current.JsonPropertyName = s_refPropertyName;
-
-                    if (converter.IsValueType)
+                    ReadOnlySpan<byte> propertyName = reader.GetSpan();
+                    switch (state.Current.LatestMetadataPropertyName = GetMetadataPropertyName(propertyName))
                     {
-                        ThrowHelper.ThrowJsonException_MetadataInvalidReferenceToValueType(converter.TypeToConvert);
+                        case MetadataPropertyName.Id:
+                            state.Current.JsonPropertyName = s_idPropertyName;
+
+                            if ((state.Current.MetadataPropertyNames & (MetadataPropertyName.Id | MetadataPropertyName.Ref)) != 0)
+                            {
+                                // No $id or $ref properties should precede $id properties.
+                                ThrowHelper.ThrowJsonException_MetadataIdIsNotFirstProperty(propertyName, ref state);
+                            }
+                            if (!converter.CanHaveMetadata)
+                            {
+                                // Should not be permitted unless the converter is capable of handling metadata.
+                                ThrowHelper.ThrowJsonException_MetadataCannotParsePreservedObjectIntoImmutable(converter.TypeToConvert);
+                            }
+
+                            break;
+
+                        case MetadataPropertyName.Ref:
+                            state.Current.JsonPropertyName = s_refPropertyName;
+
+                            if (converter.IsValueType)
+                            {
+                                // Should not be permitted if the converter is a struct.
+                                ThrowHelper.ThrowJsonException_MetadataInvalidReferenceToValueType(converter.TypeToConvert);
+                            }
+                            if (state.Current.MetadataPropertyNames != 0)
+                            {
+                                // No metadata properties should precede a $ref property.
+                                ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(reader.GetSpan(), ref state);
+                            }
+
+                            break;
+
+                        case MetadataPropertyName.Values:
+                            state.Current.JsonPropertyName = s_valuesPropertyName;
+
+                            if (state.Current.MetadataPropertyNames == MetadataPropertyName.None)
+                            {
+                                // Cannot have a $values property unless there are preceding metadata properties.
+                                ThrowHelper.ThrowJsonException_MetadataMissingIdBeforeValues(ref state, propertyName);
+                            }
+
+                            break;
+
+                        default:
+                            Debug.Assert(state.Current.LatestMetadataPropertyName == MetadataPropertyName.None);
+
+                            // Encountered a non-metadata property, exit the reader.
+                            return true;
                     }
 
-                    state.Current.ObjectState = StackFrameObjectState.ReadAheadRefValue;
-                }
-                else if (metadata == MetadataPropertyName.Values)
-                {
-                    ThrowHelper.ThrowJsonException_MetadataMissingIdBeforeValues(ref state, propertyName);
-                }
-                else
-                {
-                    Debug.Assert(metadata == MetadataPropertyName.NoMetadata);
-
-                    // Having a StartObject without metadata properties is not allowed.
-                    ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(ref state, converter.TypeToConvert, reader);
+                    state.Current.PropertyState = StackFramePropertyState.Name;
                 }
-            }
 
-            if (state.Current.ObjectState == StackFrameObjectState.ReadAheadRefValue)
-            {
-                if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadRefValue))
+                if (state.Current.PropertyState < StackFramePropertyState.ReadValue)
                 {
-                    return false;
-                }
-            }
-            else if (state.Current.ObjectState == StackFrameObjectState.ReadAheadIdValue)
-            {
-                if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadIdValue))
-                {
-                    return false;
-                }
-            }
-
-            if (state.Current.ObjectState == StackFrameObjectState.ReadRefValue)
-            {
-                if (reader.TokenType != JsonTokenType.String)
-                {
-                    ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType);
-                }
-
-                string referenceId = reader.GetString()!;
-                object value = state.ReferenceResolver.ResolveReference(referenceId);
-                ValidateValueIsCorrectType<T>(value, referenceId);
-                state.Current.ReturnValue = value;
+                    state.Current.PropertyState = StackFramePropertyState.ReadValue;
 
-                state.Current.ObjectState = StackFrameObjectState.ReadAheadRefEndObject;
-            }
-            else if (state.Current.ObjectState == StackFrameObjectState.ReadIdValue)
-            {
-                if (reader.TokenType != JsonTokenType.String)
-                {
-                    ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType);
+                    // Read the property value.
+                    if (!reader.Read())
+                    {
+                        return false;
+                    }
                 }
 
-                converter.CreateInstanceForReferenceResolver(ref reader, ref state, options);
+                Debug.Assert(state.Current.PropertyState == StackFramePropertyState.ReadValue);
 
-                string referenceId = reader.GetString()!;
-                state.ReferenceResolver.AddReference(referenceId, state.Current.ReturnValue!);
-
-                // Need to Read $values property name.
-                state.Current.ObjectState = StackFrameObjectState.ReadAheadValuesName;
-            }
-
-            // Clear the metadata property name that was set in case of failure on ResolverReference/AddReference.
-            state.Current.JsonPropertyName = null;
-
-            if (state.Current.ObjectState == StackFrameObjectState.ReadAheadRefEndObject)
-            {
-                if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadRefEndObject))
-                {
-                    return false;
-                }
-            }
-
-            if (state.Current.ObjectState == StackFrameObjectState.ReadRefEndObject)
-            {
-                if (reader.TokenType != JsonTokenType.EndObject)
+                switch (state.Current.LatestMetadataPropertyName)
                 {
-                    // We just read a property. The only valid next tokens are EndObject and PropertyName.
-                    Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
+                    case MetadataPropertyName.Id:
+                        if (reader.TokenType != JsonTokenType.String)
+                        {
+                            ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType);
+                        }
 
-                    ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(reader.GetSpan(), ref state);
-                }
+                        if (state.ReferenceId != null)
+                        {
+                            ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state);
+                        }
 
-                return true;
-            }
+                        state.ReferenceId = reader.GetString();
+                        break;
 
-            if (state.Current.ObjectState == StackFrameObjectState.ReadAheadValuesName)
-            {
-                if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadValuesName))
-                {
-                    return false;
-                }
-            }
+                    case MetadataPropertyName.Ref:
+                        if (reader.TokenType != JsonTokenType.String)
+                        {
+                            ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType);
+                        }
 
-            if (state.Current.ObjectState == StackFrameObjectState.ReadValuesName)
-            {
-                if (reader.TokenType != JsonTokenType.PropertyName)
-                {
-                    ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(ref state, converter.TypeToConvert);
-                }
+                        if (state.ReferenceId != null)
+                        {
+                            ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state);
+                        }
 
-                ReadOnlySpan<byte> propertyName = reader.GetSpan();
+                        state.ReferenceId = reader.GetString();
+                        break;
 
-                if (GetMetadataPropertyName(propertyName) != MetadataPropertyName.Values)
-                {
-                    ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(ref state, converter.TypeToConvert, reader);
-                }
+                    case MetadataPropertyName.Values:
 
-                // Remember the property in case we get an exception in one of the array elements.
-                state.Current.JsonPropertyName = s_valuesPropertyName;
+                        if (reader.TokenType != JsonTokenType.StartArray)
+                        {
+                            ThrowHelper.ThrowJsonException_MetadataValuesInvalidToken(reader.TokenType);
+                        }
 
-                state.Current.ObjectState = StackFrameObjectState.ReadAheadValuesStartArray;
-            }
+                        state.Current.PropertyState = StackFramePropertyState.None;
+                        state.Current.MetadataPropertyNames |= state.Current.LatestMetadataPropertyName;
+                        return true; // "$values" property contains the nested payload, exit the metadata reader now.
 
-            if (state.Current.ObjectState == StackFrameObjectState.ReadAheadValuesStartArray)
-            {
-                if (!TryReadAheadMetadataAndSetState(ref reader, ref state, StackFrameObjectState.ReadValuesStartArray))
-                {
-                    return false;
+                    default:
+                        Debug.Fail("Non-metadata properties should not reach this stage.");
+                        break;
                 }
-            }
-
-            if (state.Current.ObjectState == StackFrameObjectState.ReadValuesStartArray)
-            {
-                // Temporary workaround for the state machine accidentally
-                // erasing the JsonPropertyName property in certain async
-                // re-entrancy patterns.
-                state.Current.JsonPropertyName = s_valuesPropertyName;
 
-                if (reader.TokenType != JsonTokenType.StartArray)
-                {
-                    ThrowHelper.ThrowJsonException_MetadataValuesInvalidToken(reader.TokenType);
-                }
-                state.Current.ValidateEndTokenOnArray = true;
-                state.Current.ObjectState = StackFrameObjectState.CreatedObject;
+                state.Current.MetadataPropertyNames |= state.Current.LatestMetadataPropertyName;
+                state.Current.PropertyState = StackFramePropertyState.None;
+                state.Current.JsonPropertyName = null;
             }
-
-            return true;
         }
-
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private static bool TryReadAheadMetadataAndSetState(ref Utf8JsonReader reader, ref ReadStack state, StackFrameObjectState nextState)
-        {
-            // If we can't read here, the read will be completed at the root API by asking the stream for more data.
-            // Set the state so we know where to resume on re-entry.
-            state.Current.ObjectState = nextState;
-            return reader.Read();
-        }
-
         internal static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan<byte> propertyName)
         {
             if (propertyName.Length > 0 && propertyName[0] == '$')
@@ -398,10 +211,11 @@ namespace System.Text.Json
                 }
             }
 
-            return MetadataPropertyName.NoMetadata;
+            return MetadataPropertyName.None;
         }
 
-        internal static bool TryGetReferenceFromJsonElement(
+        internal static bool TryHandleReferenceFromJsonElement(
+            ref Utf8JsonReader reader,
             ref ReadStack state,
             JsonElement element,
             [NotNullWhen(true)] out object? referenceValue)
@@ -420,8 +234,30 @@ namespace System.Text.Json
                         // There are more properties in an object with $ref.
                         ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties();
                     }
+                    else if (property.EscapedNameEquals(s_idPropertyName))
+                    {
+                        if (state.ReferenceId != null)
+                        {
+                            ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state);
+                        }
+
+                        if (property.Value.ValueKind != JsonValueKind.String)
+                        {
+                            ThrowHelper.ThrowJsonException_MetadataValueWasNotString(property.Value.ValueKind);
+                        }
+
+                        object boxedElement = element;
+                        state.ReferenceResolver.AddReference(property.Value.GetString()!, boxedElement);
+                        referenceValue = boxedElement;
+                        return true;
+                    }
                     else if (property.EscapedNameEquals(s_refPropertyName))
                     {
+                        if (state.ReferenceId != null)
+                        {
+                            ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state);
+                        }
+
                         if (propertyCount > 1)
                         {
                             // $ref was found but there were other properties before.
@@ -442,18 +278,135 @@ namespace System.Text.Json
             return refMetadataFound;
         }
 
-        private static void ValidateValueIsCorrectType<T>(object value, string referenceId)
+        internal static bool TryHandleReferenceFromJsonNode(
+            ref Utf8JsonReader reader,
+            ref ReadStack state,
+            JsonNode jsonNode,
+            [NotNullWhen(true)] out object? referenceValue)
+        {
+            bool refMetadataFound = false;
+            referenceValue = default;
+
+            if (jsonNode is JsonObject jsonObject)
+            {
+                int propertyCount = 0;
+                foreach (var property in jsonObject)
+                {
+                    propertyCount++;
+                    if (refMetadataFound)
+                    {
+                        // There are more properties in an object with $ref.
+                        ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties();
+                    }
+                    else if (property.Key == "$id")
+                    {
+                        if (state.ReferenceId != null)
+                        {
+                            ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state);
+                        }
+
+                        string referenceId = ReadAsStringMetadataValue(property.Value);
+                        state.ReferenceResolver.AddReference(referenceId, jsonNode);
+                        referenceValue = jsonNode;
+                        return true;
+                    }
+                    else if (property.Key == "$ref")
+                    {
+                        if (state.ReferenceId != null)
+                        {
+                            ThrowHelper.ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(s_refPropertyName, ref reader, ref state);
+                        }
+
+                        if (propertyCount > 1)
+                        {
+                            // $ref was found but there were other properties before.
+                            ThrowHelper.ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties();
+                        }
+
+                        string referenceId = ReadAsStringMetadataValue(property.Value);
+                        referenceValue = state.ReferenceResolver.ResolveReference(referenceId);
+                        refMetadataFound = true;
+                    }
+
+                    static string ReadAsStringMetadataValue(JsonNode? jsonNode)
+                    {
+                        if (jsonNode is JsonValue jsonValue &&
+                            jsonValue.TryGetValue(out string? value) &&
+                            value is not null)
+                        {
+                            return value;
+                        }
+
+                        JsonValueKind metadataValueKind = jsonNode switch
+                        {
+                            null => JsonValueKind.Null,
+                            JsonObject => JsonValueKind.Object,
+                            JsonArray => JsonValueKind.Array,
+                            JsonValue<JsonElement> element => element.Value.ValueKind,
+                            _ => JsonValueKind.Undefined,
+                        };
+
+                        Debug.Assert(metadataValueKind != JsonValueKind.Undefined);
+                        ThrowHelper.ThrowJsonException_MetadataValueWasNotString(metadataValueKind);
+                        return null!;
+                    }
+                }
+            }
+
+            return refMetadataFound;
+        }
+
+        internal static void ValidateMetadataForObjectConverter(JsonConverter converter, ref Utf8JsonReader reader, ref ReadStack state)
+        {
+            if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Values))
+            {
+                // Object converters do not support $values metadata.
+                ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(s_valuesPropertyName, ref state, reader);
+            }
+        }
+
+        internal static void ValidateMetadataForArrayConverter(JsonConverter converter, ref Utf8JsonReader reader, ref ReadStack state)
+        {
+            switch (reader.TokenType)
+            {
+                case JsonTokenType.StartArray:
+                    Debug.Assert(state.Current.MetadataPropertyNames == MetadataPropertyName.None || state.Current.LatestMetadataPropertyName == MetadataPropertyName.Values);
+                    break;
+
+                case JsonTokenType.EndObject:
+                    if (state.Current.MetadataPropertyNames != MetadataPropertyName.Ref)
+                    {
+                        // Read the entire JSON object while parsing for metadata: for collection converters this is only legal for $ref nodes.
+                        ThrowHelper.ThrowJsonException_MetadataPreservedArrayValuesNotFound(ref state, converter.TypeToConvert);
+                    }
+                    break;
+
+                default:
+                    Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
+                    // Do not tolerate non-metadata properties in collection converters.
+                    ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(ref state, converter.TypeToConvert, reader);
+                    break;
+            }
+        }
+
+        internal static T ResolveReferenceId<T>(ref ReadStack state)
         {
+            Debug.Assert(!typeof(T).IsValueType);
+            Debug.Assert(state.ReferenceId != null);
+
+            string referenceId = state.ReferenceId;
+            object value = state.ReferenceResolver.ResolveReference(referenceId);
+            state.ReferenceId = null;
+
             try
             {
-                // No need to worry about unboxing here since T will always be a reference type at this point.
-                T _ = (T)value;
+                return (T)value;
             }
             catch (InvalidCastException)
             {
                 ThrowHelper.ThrowInvalidOperationException_MetadataReferenceOfTypeCannotBeAssignedToType(
                     referenceId, value.GetType(), typeof(T));
-                throw;
+                return default!;
             }
         }
     }
index 07b58cd..b8a6881 100644 (file)
@@ -82,7 +82,7 @@ namespace System.Text.Json
                 unescapedPropertyName = propertyName;
             }
 
-            if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve)
+            if (state.CanContainMetadata)
             {
                 if (propertyName.Length > 0 && propertyName[0] == '$')
                 {
index 68eff22..f215fc1 100644 (file)
@@ -20,13 +20,13 @@ namespace System.Text.Json
         {
             if (state.NewReferenceId != null)
             {
-                Debug.Assert(jsonConverter.CanHaveIdMetadata);
+                Debug.Assert(jsonConverter.CanHaveMetadata);
                 writer.WriteString(s_metadataId, state.NewReferenceId);
                 state.NewReferenceId = null;
                 return MetadataPropertyName.Id;
             }
 
-            return MetadataPropertyName.NoMetadata;
+            return MetadataPropertyName.None;
         }
 
         internal static MetadataPropertyName WriteReferenceForCollection(
@@ -36,7 +36,7 @@ namespace System.Text.Json
         {
             if (state.NewReferenceId != null)
             {
-                Debug.Assert(jsonConverter.CanHaveIdMetadata);
+                Debug.Assert(jsonConverter.CanHaveMetadata);
                 writer.WriteStartObject();
                 writer.WriteString(s_metadataId, state.NewReferenceId);
                 writer.WriteStartArray(s_metadataValues);
@@ -46,7 +46,7 @@ namespace System.Text.Json
 
             // If the jsonConverter supports immutable enumerables or value type collections, don't write any metadata
             writer.WriteStartArray();
-            return MetadataPropertyName.NoMetadata;
+            return MetadataPropertyName.None;
         }
 
         /// <summary>
index 4b9b0fa..112476b 100644 (file)
@@ -3,11 +3,12 @@
 
 namespace System.Text.Json
 {
-    internal enum MetadataPropertyName
+    [Flags]
+    internal enum MetadataPropertyName : byte
     {
-        NoMetadata,
-        Values,
-        Id,
-        Ref,
+        None = 0,
+        Values = 1,
+        Id = 2,
+        Ref = 4,
     }
 }
index c1a748e..023dd8b 100644 (file)
@@ -62,11 +62,21 @@ namespace System.Text.Json
         public bool SupportContinuation;
 
         /// <summary>
+        /// Holds the value of $id or $ref of the currently read object
+        /// </summary>
+        public string? ReferenceId;
+
+        /// <summary>
         /// Whether we can read without the need of saving state for stream and preserve references cases.
         /// </summary>
         public bool UseFastPath;
 
         /// <summary>
+        /// Global flag indicating whether the current deserializer supports metadata.
+        /// </summary>
+        public bool CanContainMetadata;
+
+        /// <summary>
         /// Ensures that the stack buffer has sufficient capacity to hold an additional frame.
         /// </summary>
         private void EnsurePushCapacity()
@@ -89,22 +99,18 @@ namespace System.Text.Json
 
         internal void Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinuation = false)
         {
-            Current.JsonTypeInfo = jsonTypeInfo;
-
-            // The initial JsonPropertyInfo will be used to obtain the converter.
-            Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo;
-
-            Current.NumberHandling = Current.JsonPropertyInfo.NumberHandling;
-
             JsonSerializerOptions options = jsonTypeInfo.Options;
-            bool preserveReferences = options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve;
-            if (preserveReferences)
+            if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve)
             {
                 ReferenceResolver = options.ReferenceHandler!.CreateResolver(writing: false);
+                CanContainMetadata = true;
             }
 
             SupportContinuation = supportContinuation;
-            UseFastPath = !supportContinuation && !preserveReferences;
+            Current.JsonTypeInfo = jsonTypeInfo;
+            Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo;
+            Current.NumberHandling = Current.JsonPropertyInfo.NumberHandling;
+            UseFastPath = !supportContinuation && !CanContainMetadata;
         }
 
         public void Push()
@@ -140,6 +146,7 @@ namespace System.Text.Json
                 // We are re-entering a continuation, adjust indices accordingly
                 if (_count++ > 0)
                 {
+                    _stack[_count - 2] = Current;
                     Current = _stack[_count - 1];
                 }
 
@@ -318,6 +325,24 @@ namespace System.Text.Json
             }
         }
 
+        // Traverses the stack for the outermost object being deserialized using constructor parameters
+        // Only called when calculating exception information.
+        public JsonTypeInfo GetTopJsonTypeInfoWithParameterizedConstructor()
+        {
+            Debug.Assert(!IsContinuation);
+
+            for (int i = 0; i < _count - 1; i++)
+            {
+                if (_stack[i].JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase.ConstructorIsParameterized)
+                {
+                    return _stack[i].JsonTypeInfo;
+                }
+            }
+
+            Debug.Assert(Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase.ConstructorIsParameterized);
+            return Current.JsonTypeInfo;
+        }
+
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         private void SetConstructorArgumentState()
         {
index 8af364b..948cff4 100644 (file)
@@ -36,8 +36,8 @@ namespace System.Text.Json
         public JsonTypeInfo JsonTypeInfo;
         public StackFrameObjectState ObjectState; // State tracking the current object.
 
-        // Validate EndObject token on array with preserve semantics.
-        public bool ValidateEndTokenOnArray;
+        public MetadataPropertyName LatestMetadataPropertyName;
+        public MetadataPropertyName MetadataPropertyNames;
 
         // For performance, we order the properties by the first deserialize and PropertyIndex helps find the right slot quicker.
         public int PropertyIndex;
@@ -63,7 +63,6 @@ namespace System.Text.Json
             JsonPropertyName = null;
             JsonPropertyNameAsString = null;
             PropertyState = StackFramePropertyState.None;
-            ValidateEndTokenOnArray = false;
 
             // No need to clear these since they are overwritten each time:
             //  NumberHandling
index dca579f..e0ff59c 100644 (file)
@@ -12,24 +12,8 @@ namespace System.Text.Json
         None = 0,
 
         StartToken,
-
-        ReadAheadNameOrEndObject, // Try to move the reader to the the first $id, $ref, or the EndObject token.
-        ReadNameOrEndObject, // Read the first $id, $ref, or the EndObject token.
-
-        ReadAheadIdValue, // Try to move the reader to the value for $id.
-        ReadAheadRefValue, // Try to move the reader to the value for $ref.
-        ReadIdValue, // Read value for $id.
-        ReadRefValue, // Read value for $ref.
-        ReadAheadRefEndObject, // Try to move the reader to the EndObject for $ref.
-        ReadRefEndObject, // Read the EndObject for $ref.
-
-        ReadAheadValuesName, // Try to move the reader to the $values property name.
-        ReadValuesName, // Read $values property name.
-        ReadAheadValuesStartArray, // Try to move the reader to the StartArray for $values.
-        ReadValuesStartArray, // Read the StartArray for $values.
-
-        PropertyValue, // Whether all metadata properties has been read.
-
+        ReadMetadata,
+        ConstructorArguments,
         CreatedObject,
         ReadElements,
         EndToken,
index a42f046..b34e6a9 100644 (file)
@@ -226,15 +226,16 @@ namespace System.Text.Json
         }
 
         [DoesNotReturn]
-        public static void ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotHonored(
+        public static void ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotSupported(
             ReadOnlySpan<byte> propertyName,
             ref Utf8JsonReader reader,
             ref ReadStack state)
         {
+            JsonTypeInfo jsonTypeInfo = state.GetTopJsonTypeInfoWithParameterizedConstructor();
             state.Current.JsonPropertyName = propertyName.ToArray();
 
             NotSupportedException ex = new NotSupportedException(
-                SR.Format(SR.ObjectWithParameterizedCtorRefMetadataNotHonored, state.Current.JsonTypeInfo.Type));
+                SR.Format(SR.ObjectWithParameterizedCtorRefMetadataNotSupported, jsonTypeInfo.Type));
             ThrowNotSupportedException(ref state, reader, ex);
         }
 
@@ -558,10 +559,6 @@ namespace System.Text.Json
             ref Utf8JsonReader reader,
             ref ReadStack state)
         {
-            if (state.Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase.ConstructorIsParameterized)
-            {
-                ThrowNotSupportedException_ObjectWithParameterizedCtorRefMetadataNotHonored(propertyName, ref reader, ref state);
-            }
 
             MetadataPropertyName name = JsonSerializer.GetMetadataPropertyName(propertyName);
             if (name == MetadataPropertyName.Id)
index bc7edf6..de47537 100644 (file)
@@ -78,7 +78,7 @@ namespace System.Text.Json.Serialization.Tests
 
             string exStr = ex.ToString();
             Assert.Contains("System.Text.Json.Serialization.Tests.ConstructorTests+Employee", exStr);
-            Assert.Contains("$.$id", exStr);
+            Assert.Contains("$.Manager.$ref", exStr);
         }
 
         public class Employee
@@ -107,10 +107,8 @@ namespace System.Text.Json.Serialization.Tests
 
             var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve };
 
-            NotSupportedException ex = await Assert.ThrowsAsync<NotSupportedException>(() => Serializer.DeserializeWrapper<Employee>(json, options));
-            string exStr = ex.ToString();
-            Assert.Contains("System.Text.Json.Serialization.Tests.ConstructorTests+Employee", exStr);
-            Assert.Contains("$.$random", exStr);
+            JsonException ex = await Assert.ThrowsAsync<JsonException>(() => Serializer.DeserializeWrapper<Employee>(json, options));
+            Assert.Equal("$.$random", ex.Path);
         }
 
         [Fact]
index 83e3b78..39af73f 100644 (file)
@@ -690,7 +690,7 @@ namespace System.Text.Json.Serialization.Tests
             }";
 
             JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<Employee>(json, s_deserializerOptionsPreserve));
-            Assert.Equal("$.$ref", ex.Path);
+            Assert.Equal("$", ex.Path);
         }
 
         [Fact()]
@@ -702,7 +702,7 @@ namespace System.Text.Json.Serialization.Tests
             }";
 
             JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<List<Employee>>(json, s_deserializerOptionsPreserve));
-            Assert.Equal("$.$ref", ex.Path);
+            Assert.Equal("$", ex.Path);
         }
 
         [Fact]
@@ -714,7 +714,7 @@ namespace System.Text.Json.Serialization.Tests
             }";
 
             JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<Dictionary<string, Employee>>(json, s_deserializerOptionsPreserve));
-            Assert.Equal("$.$ref", ex.Path);
+            Assert.Equal("$", ex.Path);
         }
 
         [Fact]
@@ -728,7 +728,7 @@ namespace System.Text.Json.Serialization.Tests
             }";
 
             JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<Employee>(json, s_deserializerOptionsPreserve));
-            Assert.Equal("$.Manager.$ref", ex.Path);
+            Assert.Equal("$.Manager", ex.Path);
         }
 
         [Fact]
@@ -742,7 +742,7 @@ namespace System.Text.Json.Serialization.Tests
             }";
 
             JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<Employee>(json, s_deserializerOptionsPreserve));
-            Assert.Equal("$.Subordinates.$ref", ex.Path);
+            Assert.Equal("$.Subordinates", ex.Path);
         }
 
         [Fact]
@@ -756,7 +756,7 @@ namespace System.Text.Json.Serialization.Tests
             }";
 
             JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<Employee>(json, s_deserializerOptionsPreserve));
-            Assert.Equal("$.Contacts.$ref", ex.Path);
+            Assert.Equal("$.Contacts", ex.Path);
         }
 
         #endregion
@@ -1235,7 +1235,7 @@ namespace System.Text.Json.Serialization.Tests
 
             JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<List<Employee>>(json, s_deserializerOptionsPreserve));
             Assert.Contains("'1'", ex.Message);
-            Assert.Equal("$[0].$ref", ex.Path);
+            Assert.Equal("$[0]", ex.Path);
         }
 
         [Theory]
@@ -1315,7 +1315,7 @@ namespace System.Text.Json.Serialization.Tests
 
             JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<List<Employee>>(json, s_deserializerOptionsPreserve));
 
-            Assert.Equal("$[1].$id", ex.Path);
+            Assert.Equal("$[1]", ex.Path);
             Assert.Contains("'1'", ex.Message);
         }
 
@@ -1341,7 +1341,7 @@ namespace System.Text.Json.Serialization.Tests
 
             JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<ClassWithTwoListProperties>(json, s_deserializerOptionsPreserve));
 
-            Assert.Equal("$.List2.$id", ex.Path);
+            Assert.Equal("$.List2.$values", ex.Path);
             Assert.Contains("'1'", ex.Message);
         }
 
@@ -1520,7 +1520,7 @@ namespace System.Text.Json.Serialization.Tests
             }";
 
             JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<List<string>>(json, s_deserializerOptionsPreserve));
-            Assert.Equal("$.$ref", ex.Path);
+            Assert.Equal("$", ex.Path);
 
             // $id Valid under conditions: must be the first property in the object.
             // $values Valid under conditions: must be after $id.
@@ -1572,5 +1572,85 @@ namespace System.Text.Json.Serialization.Tests
 
         public class Derived : Base { }
         public class Base { }
+
+        [Theory]
+        [InlineData(JsonUnknownTypeHandling.JsonElement)]
+        [InlineData(JsonUnknownTypeHandling.JsonNode)]
+        public async Task ObjectConverter_ShouldHandleReferenceMetadata(JsonUnknownTypeHandling typehandling)
+        {
+            var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, UnknownTypeHandling = typehandling };
+            string json = @"[{ ""$id"" : ""1"" },{ ""$ref"" : ""1""}]";
+            object[] deserialized = await Serializer.DeserializeWrapper<object[]>(json, options);
+            Assert.Same(deserialized[0], deserialized[1]);
+        }
+
+        [Theory]
+        [InlineData(@"{ ""$id""  : 42 }", JsonUnknownTypeHandling.JsonElement)]
+        [InlineData(@"{ ""$id""  : 42 }", JsonUnknownTypeHandling.JsonNode)]
+        [InlineData(@"{ ""$ref"" : 42 }", JsonUnknownTypeHandling.JsonElement)]
+        [InlineData(@"{ ""$ref"" : 42 }", JsonUnknownTypeHandling.JsonNode)]
+        public async Task ObjectConverter_InvalidMetadataPropertyType_ShouldThrowJsonException(string json, JsonUnknownTypeHandling typehandling)
+        {
+            var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, UnknownTypeHandling = typehandling };
+            await Assert.ThrowsAsync<JsonException>(() => Serializer.DeserializeWrapper<object>(json, options));
+        }
+
+        [Theory]
+        [InlineData(JsonUnknownTypeHandling.JsonElement)]
+        [InlineData(JsonUnknownTypeHandling.JsonNode)]
+        public async Task ObjectConverter_PropertyTrailingRefMetadata_ShouldThrowJsonException(JsonUnknownTypeHandling typehandling)
+        {
+            var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, UnknownTypeHandling = typehandling };
+            string json = @"[{ ""$id"" : ""1"" }, { ""$ref"" : ""1"", ""trailingProperty"" : true }]";
+            await Assert.ThrowsAsync<JsonException>(() => Serializer.DeserializeWrapper<object[]>(json, options));
+        }
+
+        [Fact]
+        public async Task ConstructorDeserialization_ReferencePreservation()
+        {
+            string json = @"[{ ""$id"" : ""1"", ""Value"" : 42, ""Next"" : null }, { ""$ref"" : ""1"" }]";
+            LinkedList<int>[] deserialized = await Serializer.DeserializeWrapper<LinkedList<int>[]>(json, s_serializerOptionsPreserve);
+
+            Assert.Equal(2, deserialized.Length);
+            Assert.Equal(42, deserialized[0].Value);
+            Assert.Null(deserialized[0].Next);
+            Assert.Same(deserialized[0], deserialized[1]);
+        }
+
+        [Theory]
+        [InlineData(@"{ ""Value"" : 1, ""Next"" : { ""$id"" : ""1"", ""Value"" : 2, ""Next"" : null }}", typeof(LinkedList<int>))]
+        [InlineData(@"[{ ""$id"" : ""1"", ""Value"" : 2, ""Next"" : null }, { ""Value"" : 1, ""Next"" : { ""$ref"" : ""1""}}]", typeof(LinkedList<int>[]))]
+        public Task ConstructorDeserialization_NestedConstructorArgumentReference_SupportedScenaria(string json, Type type)
+            => Serializer.DeserializeWrapper(json, type, s_serializerOptionsPreserve);
+
+        [Theory]
+        [InlineData(@"{ ""$id"" : ""1"", ""Value"" : 1, ""Next"" : { ""$id"" : ""2"", ""Value"" : 2, ""Next"" : null }}", typeof(LinkedList<int>))]
+        [InlineData(@"{ ""$id"" : ""1"", ""Value"" : 1, ""Next"" : { ""$ref"" : ""1"" }}", typeof(LinkedList<int>))]
+        [InlineData(@"[{ ""$id"" : ""1"", ""Value"" : 2, ""Next"" : null }, { ""$id"" : ""2"", ""Value"" : 1, ""Next"" : { ""$ref"" : ""1""}}]", typeof(LinkedList<int>[]))]
+        [InlineData(@"{ ""$id"" : ""1"", ""Value"" : [{""$id"" : ""2""}], ""Next"" : null }", typeof(LinkedList<Base[]>))]
+        [InlineData(@"{ ""$id"" : ""1"", ""Value"" : [[{""$id"" : ""2""}]], ""Next"" : null }", typeof(LinkedList<Base[][]>))]
+        [InlineData(@"{ ""$id"" : ""1"", ""Value"" : [{""$ref"" : ""1""}], ""Next"" : null }", typeof(LinkedList<object[]>))]
+        [InlineData(@"{ ""$id"" : ""1"", ""PropertyWithSetter"" : { ""$id"" : ""2"" }}", typeof(LinkedList<object?>))]
+        [InlineData(@"{ ""$id"" : ""1"", ""PropertyWithSetter"" : { ""$ref"" : ""1"" }}", typeof(LinkedList<object?>))]
+        public async Task ConstructorDeserialization_NestedConstructorArgumentReference_ThrowsNotSupportedException(string json, Type type)
+        {
+            NotSupportedException ex = await Assert.ThrowsAsync<NotSupportedException>(() => Serializer.DeserializeWrapper(json, type, s_serializerOptionsPreserve));
+            Assert.Contains("LinkedList", ex.Message);
+        }
+
+        public class LinkedList<T>
+        {
+            [JsonConstructor]
+            public LinkedList(T value, LinkedList<T>? next)
+            {
+                Value = value;
+                Next = next;
+            }
+
+            public T Value { get; }
+            public LinkedList<T>? Next { get; }
+
+            public T? PropertyWithSetter { get; set; }
+        }
     }
 }
index a2467d8..562b92f 100644 (file)
@@ -739,26 +739,6 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         [Fact]
-        public async Task PreserveReferenceOfTypeObjectAsync()
-        {
-            if (StreamingSerializer is null)
-            {
-                return;
-            }
-
-            var root = new ClassWithObjectProperty();
-            root.Child = new ClassWithObjectProperty();
-            root.Sibling = root.Child;
-
-            Assert.Same(root.Child, root.Sibling);
-
-            string json = await StreamingSerializer.SerializeWrapper(root, s_serializerOptionsPreserve);
-
-            ClassWithObjectProperty rootCopy = await StreamingSerializer.DeserializeWrapper<ClassWithObjectProperty>(json, s_serializerOptionsPreserve);
-            Assert.Same(rootCopy.Child, rootCopy.Sibling);
-        }
-
-        [Fact]
         public async Task PreserveReferenceOfTypeOfObjectOnCollection()
         {
             var root = new ClassWithListOfObjectProperty();
index 9ffd634..9c2996b 100644 (file)
@@ -129,6 +129,12 @@ namespace System.Text.Json.SourceGeneration.Tests
         [JsonSerializable(typeof(List<object>))]
         [JsonSerializable(typeof(StructCollection))]
         [JsonSerializable(typeof(ImmutableArray<int>))]
+        [JsonSerializable(typeof(LinkedList<int>))]
+        [JsonSerializable(typeof(LinkedList<int>[]))]
+        [JsonSerializable(typeof(LinkedList<object>))]
+        [JsonSerializable(typeof(LinkedList<object[]>))]
+        [JsonSerializable(typeof(LinkedList<Base[]>))]
+        [JsonSerializable(typeof(LinkedList<Base[][]>))]
         internal sealed partial class ReferenceHandlerTestsContext_Metadata : JsonSerializerContext
         {
         }
@@ -261,6 +267,12 @@ namespace System.Text.Json.SourceGeneration.Tests
         [JsonSerializable(typeof(List<object>))]
         [JsonSerializable(typeof(StructCollection))]
         [JsonSerializable(typeof(ImmutableArray<int>))]
+        [JsonSerializable(typeof(LinkedList<int>))]
+        [JsonSerializable(typeof(LinkedList<int>[]))]
+        [JsonSerializable(typeof(LinkedList<object>))]
+        [JsonSerializable(typeof(LinkedList<object[]>))]
+        [JsonSerializable(typeof(LinkedList<Base[]>))]
+        [JsonSerializable(typeof(LinkedList<Base[][]>))]
         internal sealed partial class ReferenceHandlerTestsContext_Default : JsonSerializerContext
         {
         }