Support async KeyValuePair (de)serialization (#36607)
authorLayomi Akinrinade <laakinri@microsoft.com>
Thu, 28 May 2020 17:15:29 +0000 (13:15 -0400)
committerGitHub <noreply@github.com>
Thu, 28 May 2020 17:15:29 +0000 (10:15 -0700)
* Support async KeyValuePair (de)serialization

* Address review feedback

* Fix up test

16 files changed:
src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ArgumentState.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.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/Converters/Value/KeyValuePairConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/KeyValuePairConverterFactory.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Utf8JsonReader.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Utf8JsonWriter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonValueConverterOfT.cs [deleted file]
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/tests/Serialization/CollectionTests/CollectionTests.KeyValuePair.cs
src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.DictionaryInt32StringKeyValueConverter.cs
src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.DictionaryKeyValueConverter.cs
src/libraries/System.Text.Json/tests/Serialization/OptionsTests.cs
src/libraries/System.Text.Json/tests/Serialization/Stream.Collections.cs

index 7fcf718..7c82fc4 100644 (file)
     <NoWarn Condition="'$(TargetFramework)' == 'netstandard2.0' or $(TargetFramework.StartsWith('net4'))">$(NoWarn);nullable</NoWarn>
   </PropertyGroup>
   <ItemGroup>
-    <Compile Include="$(CommonPath)System\Runtime\CompilerServices\PreserveDependencyAttribute.cs"
-             Link="Common\System\Runtime\CompilerServices\PreserveDependencyAttribute.cs" />
-    <Compile Include="$(CommonPath)System\HexConverter.cs"
-             Link="Common\System\HexConverter.cs" />
+    <Compile Include="$(CommonPath)System\Runtime\CompilerServices\PreserveDependencyAttribute.cs" Link="Common\System\Runtime\CompilerServices\PreserveDependencyAttribute.cs" />
+    <Compile Include="$(CommonPath)System\HexConverter.cs" Link="Common\System\HexConverter.cs" />
     <Compile Include="System\Text\Json\BitStack.cs" />
     <Compile Include="System\Text\Json\Document\JsonDocument.cs" />
     <Compile Include="System\Text\Json\Document\JsonDocument.DbRow.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializerOptions.Converters.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonSerializerOptions.cs" />
     <Compile Include="System\Text\Json\Serialization\JsonStringEnumConverter.cs" />
-    <Compile Include="System\Text\Json\Serialization\JsonValueConverterOfT.cs" />
     <Compile Include="System\Text\Json\Serialization\MemberAccessor.cs" />
     <Compile Include="System\Text\Json\Serialization\MetadataPropertyName.cs" />
     <Compile Include="System\Text\Json\Serialization\ParameterRef.cs" />
   <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or $(TargetFramework.StartsWith('net4'))">
     <Compile Include="System\Collections\Generic\StackExtensions.netstandard.cs" />
     <!-- Common or Common-branched source files -->
-    <Compile Include="$(CommonPath)System\Buffers\ArrayBufferWriter.cs"
-             Link="Common\System\Buffers\ArrayBufferWriter.cs" />
+    <Compile Include="$(CommonPath)System\Buffers\ArrayBufferWriter.cs" Link="Common\System\Buffers\ArrayBufferWriter.cs" />
     <Reference Include="mscorlib" />
     <Reference Include="System" />
     <Reference Include="System.Core" />
     <Reference Include="Microsoft.Bcl.AsyncInterfaces" />
   </ItemGroup>
   <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or $(TargetFramework.StartsWith('net4')) or '$(TargetFramework)' == 'netcoreapp3.0'">
-    <Compile Include="$(CommonPath)System\Collections\Generic\ReferenceEqualityComparer.cs"
-             Link="Common\System\Collections\Generic\ReferenceEqualityComparer.cs" />
+    <Compile Include="$(CommonPath)System\Collections\Generic\ReferenceEqualityComparer.cs" Link="Common\System\Collections\Generic\ReferenceEqualityComparer.cs" />
   </ItemGroup>
   <ItemGroup Condition="'$(TargetFramework)' == '$(NetCoreAppCurrent)' or '$(TargetFramework)' == 'netcoreapp3.0'">
     <Reference Include="System.Collections" />
index 71f4d2f..8a63594 100644 (file)
@@ -31,5 +31,9 @@ namespace System.Text.Json
         // For performance, we order the parameters by the first deserialize and PropertyIndex helps find the right slot quicker.
         public int ParameterIndex;
         public List<ParameterRef>? ParameterRefCache;
+
+        // Used when deserializing KeyValuePair instances.
+        public bool FoundKey;
+        public bool FoundValue;
     }
 }
index 1e98384..f79b5f6 100644 (file)
@@ -10,7 +10,7 @@ namespace System.Text.Json.Serialization.Converters
     /// Implementation of <cref>JsonObjectConverter{T}</cref> that supports the deserialization
     /// of JSON objects using parameterized constructors.
     /// </summary>
-    internal sealed class SmallObjectWithParameterizedConstructorConverter<T, TArg0, TArg1, TArg2, TArg3> : ObjectWithParameterizedConstructorConverter<T> where T : notnull
+    internal class SmallObjectWithParameterizedConstructorConverter<T, TArg0, TArg1, TArg2, TArg3> : ObjectWithParameterizedConstructorConverter<T> where T : notnull
     {
         protected override object CreateObject(ref ReadStackFrame frame)
         {
index 54e49b3..1af7ab0 100644 (file)
@@ -135,6 +135,8 @@ namespace System.Text.Json.Serialization.Converters
                 state.Current.JsonClassInfo.UpdateSortedParameterCache(ref state.Current);
             }
 
+            EndRead(ref state);
+
             value = (T)obj;
 
             return true;
@@ -440,11 +442,12 @@ namespace System.Text.Json.Serialization.Converters
             InitializeConstructorArgumentCaches(ref state, options);
         }
 
+        protected virtual void EndRead(ref ReadStack state) { }
+
         /// <summary>
         /// Lookup the constructor parameter given its name in the reader.
         /// </summary>
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private bool TryLookupConstructorParameter(
+        protected virtual bool TryLookupConstructorParameter(
             ref ReadStack state,
             ref Utf8JsonReader reader,
             JsonSerializerOptions options,
index 08e7907..3d8a4be 100644 (file)
@@ -3,31 +3,29 @@
 // See the LICENSE file in the project root for more information.
 
 using System.Collections.Generic;
-using System.Text.Encodings.Web;
+using System.Diagnostics;
+using System.Reflection;
 
 namespace System.Text.Json.Serialization.Converters
 {
-    internal sealed class KeyValuePairConverter<TKey, TValue> : JsonValueConverter<KeyValuePair<TKey, TValue>>
+    internal sealed class KeyValuePairConverter<TKey, TValue> :
+        SmallObjectWithParameterizedConstructorConverter<KeyValuePair<TKey, TValue>, TKey, TValue, object, object>
     {
         private const string KeyNameCLR = "Key";
         private const string ValueNameCLR = "Value";
 
+        private const int NumProperties = 2;
+
         // Property name for "Key" and "Value" with Options.PropertyNamingPolicy applied.
         private string _keyName = null!;
         private string _valueName = null!;
 
-        // _keyName and _valueName as JsonEncodedText.
-        private JsonEncodedText _keyNameEncoded;
-        private JsonEncodedText _valueNameEncoded;
-
-        // todo: https://github.com/dotnet/runtime/issues/32352
-        // it is possible to cache the underlying converters since this is an internal converter and
-        // an instance is created only once for each JsonSerializerOptions instance.
+        private static readonly ConstructorInfo s_constructorInfo =
+            typeof(KeyValuePair<TKey, TValue>).GetConstructor(new[] { typeof(TKey), typeof(TValue) })!;
 
         internal override void Initialize(JsonSerializerOptions options)
         {
             JsonNamingPolicy? namingPolicy = options.PropertyNamingPolicy;
-
             if (namingPolicy == null)
             {
                 _keyName = KeyNameCLR;
@@ -38,107 +36,68 @@ namespace System.Text.Json.Serialization.Converters
                 _keyName = namingPolicy.ConvertName(KeyNameCLR);
                 _valueName = namingPolicy.ConvertName(ValueNameCLR);
 
-                if (_keyName == null || _valueName == null)
-                {
-                    ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(namingPolicy);
-                }
+                // Validation for the naming policy will occur during JsonPropertyInfo creation.
             }
 
-            JavaScriptEncoder? encoder = options.Encoder;
-            _keyNameEncoded = JsonEncodedText.Encode(_keyName, encoder);
-            _valueNameEncoded = JsonEncodedText.Encode(_valueName, encoder);
+            ConstructorInfo = s_constructorInfo;
+            Debug.Assert(ConstructorInfo != null);
         }
 
-        internal override bool OnTryRead(
-            ref Utf8JsonReader reader,
-            Type typeToConvert, JsonSerializerOptions options,
+        /// <summary>
+        /// Lookup the constructor parameter given its name in the reader.
+        /// </summary>
+        protected override bool TryLookupConstructorParameter(
             ref ReadStack state,
-            out KeyValuePair<TKey, TValue> value)
+            ref Utf8JsonReader reader,
+            JsonSerializerOptions options,
+            out JsonParameterInfo? jsonParameterInfo)
         {
-            if (reader.TokenType != JsonTokenType.StartObject)
-            {
-                ThrowHelper.ThrowJsonException();
-            }
-
-            TKey k = default!;
-            bool keySet = false;
+            JsonClassInfo classInfo = state.Current.JsonClassInfo;
+            ArgumentState? argState = state.Current.CtorArgumentState;
 
-            TValue v = default!;
-            bool valueSet = false;
-
-            // Get the first property.
-            reader.ReadWithVerify();
-            if (reader.TokenType != JsonTokenType.PropertyName)
-            {
-                ThrowHelper.ThrowJsonException();
-            }
+            Debug.Assert(classInfo.ClassType == ClassType.Object);
+            Debug.Assert(argState != null);
+            Debug.Assert(_keyName != null);
+            Debug.Assert(_valueName != null);
 
             bool caseInsensitiveMatch = options.PropertyNameCaseInsensitive;
 
             string propertyName = reader.GetString()!;
-            if (FoundKeyProperty(propertyName, caseInsensitiveMatch))
-            {
-                reader.ReadWithVerify();
-                k = JsonSerializer.Deserialize<TKey>(ref reader, options, ref state, _keyName);
-                keySet = true;
-            }
-            else if (FoundValueProperty(propertyName, caseInsensitiveMatch))
-            {
-                reader.ReadWithVerify();
-                v = JsonSerializer.Deserialize<TValue>(ref reader, options, ref state, _valueName);
-                valueSet = true;
-            }
-            else
-            {
-                ThrowHelper.ThrowJsonException();
-            }
+            state.Current.JsonPropertyNameAsString = propertyName;
 
-            // Get the second property.
-            reader.ReadWithVerify();
-            if (reader.TokenType != JsonTokenType.PropertyName)
+            if (!argState.FoundKey &&
+                FoundKeyProperty(propertyName, caseInsensitiveMatch))
             {
-                ThrowHelper.ThrowJsonException();
+                jsonParameterInfo = classInfo.ParameterCache![_keyName];
+                argState.FoundKey = true;
             }
-
-            propertyName = reader.GetString()!;
-            if (!keySet && FoundKeyProperty(propertyName, caseInsensitiveMatch))
+            else if (!argState.FoundValue &&
+                FoundValueProperty(propertyName, caseInsensitiveMatch))
             {
-                reader.ReadWithVerify();
-                k = JsonSerializer.Deserialize<TKey>(ref reader, options, ref state, _keyName);
-            }
-            else if (!valueSet && FoundValueProperty(propertyName, caseInsensitiveMatch))
-            {
-                reader.ReadWithVerify();
-                v = JsonSerializer.Deserialize<TValue>(ref reader, options, ref state, _valueName);
+                jsonParameterInfo = classInfo.ParameterCache![_valueName];
+                argState.FoundValue = true;
             }
             else
             {
                 ThrowHelper.ThrowJsonException();
+                jsonParameterInfo = null;
+                return false;
             }
 
-            reader.ReadWithVerify();
-
-            if (reader.TokenType != JsonTokenType.EndObject)
-            {
-                ThrowHelper.ThrowJsonException();
-            }
-
-            value = new KeyValuePair<TKey, TValue>(k!, v!);
+            Debug.Assert(jsonParameterInfo != null);
+            argState.ParameterIndex++;
+            argState.JsonParameterInfo = jsonParameterInfo;
             return true;
         }
 
-        internal override bool OnTryWrite(Utf8JsonWriter writer, KeyValuePair<TKey, TValue> value, JsonSerializerOptions options, ref WriteStack state)
+        protected override void EndRead(ref ReadStack state)
         {
-            writer.WriteStartObject();
-
-            writer.WritePropertyName(_keyNameEncoded);
-            JsonSerializer.Serialize(writer, value.Key, options, ref state, _keyName);
-
-            writer.WritePropertyName(_valueNameEncoded);
-            JsonSerializer.Serialize(writer, value.Value, options, ref state, _valueName);
+            Debug.Assert(state.Current.PropertyIndex == 0);
 
-            writer.WriteEndObject();
-            return true;
+            if (state.Current.CtorArgumentState!.ParameterIndex != NumProperties)
+            {
+                ThrowHelper.ThrowJsonException();
+            }
         }
 
         private bool FoundKeyProperty(string propertyName, bool caseInsensitiveMatch)
index 672c987..2ba7c5f 100644 (file)
@@ -3,6 +3,7 @@
 // See the LICENSE file in the project root for more information.
 
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Reflection;
 using System.Runtime.CompilerServices;
 
@@ -22,6 +23,8 @@ namespace System.Text.Json.Serialization.Converters
         [PreserveDependency(".ctor()", "System.Text.Json.Serialization.Converters.KeyValuePairConverter`2")]
         public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
         {
+            Debug.Assert(CanConvert(type));
+
             Type keyType = type.GetGenericArguments()[0];
             Type valueType = type.GetGenericArguments()[1];
 
index 666a684..b6395bd 100644 (file)
@@ -12,31 +12,6 @@ namespace System.Text.Json
     public static partial class JsonSerializer
     {
         /// <summary>
-        /// Internal version that allows re-entry with preserving ReadStack so that JsonPath works correctly.
-        /// </summary>
-        [return: MaybeNull]
-        internal static TValue Deserialize<TValue>(ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state, string? propertyName = null)
-        {
-            if (options == null)
-            {
-                throw new ArgumentNullException(nameof(options));
-            }
-
-            state.Current.InitializeReEntry(typeof(TValue), options, propertyName);
-
-            JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo!;
-
-            JsonConverter<TValue> converter = (JsonConverter<TValue>)jsonPropertyInfo.ConverterBase;
-            bool success = converter.TryRead(ref reader, jsonPropertyInfo.RuntimePropertyType!, options, ref state, out TValue value);
-            Debug.Assert(success);
-
-            // Clear the current property state since we are done processing it.
-            state.Current.EndProperty();
-
-            return value;
-        }
-
-        /// <summary>
         /// Reads one JSON value (including objects or arrays) from the provided reader into a <typeparamref name="TValue"/>.
         /// </summary>
         /// <returns>A <typeparamref name="TValue"/> representation of the JSON value.</returns>
index 3588aa0..6b364dd 100644 (file)
@@ -10,22 +10,6 @@ namespace System.Text.Json
     public static partial class JsonSerializer
     {
         /// <summary>
-        /// Internal version that allows re-entry with preserving WriteStack so that JsonPath works correctly.
-        /// </summary>
-        // If this is made public, we will also want to have a non-generic version.
-        internal static void Serialize<T>(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state, string? propertyName = null)
-        {
-            if (options == null)
-            {
-                throw new ArgumentNullException(nameof(options));
-            }
-
-            JsonConverter jsonConverter = state.Current.InitializeReEntry(typeof(T), options, propertyName);
-            bool success = jsonConverter.TryWriteAsObject(writer, value, options, ref state);
-            Debug.Assert(success);
-        }
-
-        /// <summary>
         /// Write one JSON value (including objects or arrays) to the provided writer.
         /// </summary>
         /// <param name="writer">The writer to write.</param>
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonValueConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonValueConverterOfT.cs
deleted file mode 100644 (file)
index 67fa8c4..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System.Diagnostics.CodeAnalysis;
-
-namespace System.Text.Json.Serialization
-{
-    // Used for value converters that need to re-enter the serializer since it will support JsonPath
-    // and reference handling.
-    internal abstract class JsonValueConverter<T> : JsonConverter<T>
-    {
-        internal sealed override ClassType ClassType => ClassType.NewValue;
-
-        public sealed override bool HandleNull => false;
-
-        [return: MaybeNull]
-        public sealed override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-        {
-            // Bridge from resumable to value converters.
-            if (options == null)
-            {
-                options = JsonSerializerOptions.s_defaultOptions;
-            }
-
-            ReadStack state = default;
-            state.Initialize(typeToConvert, options, supportContinuation: false);
-            TryRead(ref reader, typeToConvert, options, ref state, out T value);
-            return value;
-        }
-
-        public sealed override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
-        {
-            // Bridge from resumable to value converters.
-            if (options == null)
-            {
-                options = JsonSerializerOptions.s_defaultOptions;
-            }
-
-            WriteStack state = default;
-            state.Initialize(typeof(T), options, supportContinuation: false);
-            TryWrite(writer, value, options, ref state);
-        }
-    }
-}
index 7e7f497..8914c9c 100644 (file)
@@ -297,16 +297,18 @@ namespace System.Text.Json
                 byte[]? utf8PropertyName = frame.JsonPropertyName;
                 if (utf8PropertyName == null)
                 {
-                    // Attempt to get the JSON property name from the JsonPropertyInfo or JsonParameterInfo.
-                    utf8PropertyName = frame.JsonPropertyInfo?.NameAsUtf8Bytes ??
-                        frame.CtorArgumentState?.JsonParameterInfo?.NameAsUtf8Bytes;
-
-                    if (utf8PropertyName == null)
+                    if (frame.JsonPropertyNameAsString != null)
                     {
                         // Attempt to get the JSON property name set manually for dictionary
-                        // keys and serializer re-entry cases where a property is specified.
+                        // keys and KeyValuePair property names.
                         propertyName = frame.JsonPropertyNameAsString;
                     }
+                    else
+                    {
+                        // Attempt to get the JSON property name from the JsonPropertyInfo or JsonParameterInfo.
+                        utf8PropertyName = frame.JsonPropertyInfo?.NameAsUtf8Bytes ??
+                            frame.CtorArgumentState?.JsonParameterInfo?.NameAsUtf8Bytes;
+                    }
                 }
 
                 if (utf8PropertyName != null)
index e7d8582..d9a5947 100644 (file)
@@ -67,17 +67,6 @@ namespace System.Text.Json
             PropertyState = StackFramePropertyState.None;
         }
 
-        public void InitializeReEntry(Type type, JsonSerializerOptions options, string? propertyName)
-        {
-            JsonClassInfo jsonClassInfo = options.GetOrAddClass(type);
-
-            // The initial JsonPropertyInfo will be used to obtain the converter.
-            JsonPropertyInfo = jsonClassInfo.PropertyInfoForClassInfo;
-
-            // Set for exception handling calculation of JsonPath.
-            JsonPropertyNameAsString = propertyName;
-        }
-
         /// <summary>
         /// Is the current object a Dictionary.
         /// </summary>
index 8681670..d5b5b14 100644 (file)
@@ -327,8 +327,8 @@ namespace System.Text.Json.Serialization.Tests
                 PropertyNamingPolicy = new LeadingUnderscorePolicy() // Key -> _Key, Value -> _Value
             };
 
-            // Although policy won't produce this JSON string, the serializer parses the properties
-            // as "Key" and "Value" are special cased to accomodate content serialized with previous
+            // Although the policy won't produce these strings, the serializer successfully parses the properties.
+            // "Key" and "Value" are special cased to accomodate content serialized with previous
             // versions of the serializer (.NET Core 3.x/System.Text.Json 4.7.x).
             string json = @"{""Key"":""Hello, World!"",""Value"":1}";
             KeyValuePair<string, int> kvp = JsonSerializer.Deserialize<KeyValuePair<string, int>>(json, options);
@@ -339,7 +339,7 @@ namespace System.Text.Json.Serialization.Tests
             json = @"{""key"":""Hello, World!"",""value"":1}";
             Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<KeyValuePair<string, int>>(json, options));
 
-            // "Key" and "Value" matching is case sensitive, even when case sensitivity is on.
+            // "Key" and "Value" matching is case sensitive, even when case insensitivity is on.
             // Case sensitivity only applies to the result of converting the CLR property names
             // (Key -> _Key, Value -> _Value) with the naming policy.
             options = new JsonSerializerOptions
@@ -387,9 +387,9 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         [Theory]
-        [InlineData(typeof(KeyNameNullPolicy))]
-        [InlineData(typeof(ValueNameNullPolicy))]
-        public static void InvalidPropertyNameFail(Type policyType)
+        [InlineData(typeof(KeyNameNullPolicy), "Key")]
+        [InlineData(typeof(ValueNameNullPolicy), "Value")]
+        public static void InvalidPropertyNameFail(Type policyType, string offendingProperty)
         {
             var options = new JsonSerializerOptions
             {
@@ -398,7 +398,7 @@ namespace System.Text.Json.Serialization.Tests
 
             InvalidOperationException ex = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<KeyValuePair<string, string>>("", options));
             string exAsStr = ex.ToString();
-            Assert.Contains(policyType.ToString(), exAsStr);
+            Assert.Contains(offendingProperty, exAsStr);
 
             Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new KeyValuePair<string, string>("", ""), options));
         }
@@ -424,15 +424,56 @@ namespace System.Text.Json.Serialization.Tests
         [InlineData("{0")]
         [InlineData(@"{""Random"":")]
         [InlineData(@"{""Value"":1}")]
+        [InlineData(@"{null:1}")]
         [InlineData(@"{""Value"":1,2")]
         [InlineData(@"{""Value"":1,""Random"":")]
         [InlineData(@"{""Key"":1,""Key"":1}")]
+        [InlineData(@"{null:1,""Key"":1}")]
         [InlineData(@"{""Key"":1,""Key"":2}")]
         [InlineData(@"{""Value"":1,""Value"":1}")]
+        [InlineData(@"{""Value"":1,null:1}")]
         [InlineData(@"{""Value"":1,""Value"":2}")]
         public static void InvalidJsonFail(string json)
         {
             Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<KeyValuePair<int, int>>(json));
         }
+
+        [Theory]
+        [InlineData(@"{""Key"":""1"",""Value"":2}", "$.Key")]
+        [InlineData(@"{""Key"":1,""Value"":""2""}", "$.Value")]
+        [InlineData(@"{""key"":1,""Value"":2}", "$.key")]
+        [InlineData(@"{""Key"":1,""value"":2}", "$.value")]
+        [InlineData(@"{""Extra"":3,""Key"":1,""Value"":2}", "$.Extra")]
+        [InlineData(@"{""Key"":1,""Extra"":3,""Value"":2}", "$.Extra")]
+        [InlineData(@"{""Key"":1,""Value"":2,""Extra"":3}", "$.Extra")]
+        public static void JsonPathIsAccurate(string json, string expectedPath)
+        {
+            JsonException ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<KeyValuePair<int, int>>(json));
+            Assert.Contains(expectedPath, ex.ToString());
+
+            var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+            ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<KeyValuePair<int, int>>(json));
+            Assert.Contains(expectedPath, ex.ToString());
+        }
+
+        [Theory]
+        [InlineData(@"{""kEy"":""1"",""vAlUe"":2}", "$.kEy")]
+        [InlineData(@"{""kEy"":1,""vAlUe"":""2""}", "$.vAlUe")]
+        public static void JsonPathIsAccurate_CaseInsensitive(string json, string expectedPath)
+        {
+            var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+            JsonException ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<KeyValuePair<int, int>>(json, options));
+            Assert.Contains(expectedPath, ex.ToString());
+        }
+
+        [Theory]
+        [InlineData(@"{""_Key"":""1"",""_Value"":2}", "$._Key")]
+        [InlineData(@"{""_Key"":1,""_Value"":""2""}", "$._Value")]
+        public static void JsonPathIsAccurate_PropertyNamingPolicy(string json, string expectedPath)
+        {
+            var options = new JsonSerializerOptions { PropertyNamingPolicy = new LeadingUnderscorePolicy() };
+            JsonException ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<KeyValuePair<int, int>>(json, options));
+            Assert.Contains(expectedPath, ex.ToString());
+        }
     }
 }
index bfdf8a3..ea4a3c2 100644 (file)
@@ -47,7 +47,7 @@ namespace System.Text.Json.Serialization.Tests
                         return value;
                     }
 
-                    KeyValuePair<int, string> kvpair = _intToStringConverter.Read(ref reader, typeToConvert, options);
+                    KeyValuePair<int, string> kvpair = _intToStringConverter.Read(ref reader, typeof(KeyValuePair<int, string>), options);
 
                     value.Add(kvpair.Key, kvpair.Value);
                 }
index c245d7d..7dda6e4 100644 (file)
@@ -82,7 +82,7 @@ namespace System.Text.Json.Serialization.Tests
                             return value;
                         }
 
-                        KeyValuePair<TKey, TValue> kv = _converter.Read(ref reader, typeToConvert, options);
+                        KeyValuePair<TKey, TValue> kv = _converter.Read(ref reader, typeof(KeyValuePair<TKey, TValue>), options);
                         value.Add(kv.Key, kv.Value);
                     }
 
index c4fe9c4..e86537c 100644 (file)
@@ -403,12 +403,22 @@ namespace System.Text.Json.Serialization.Tests
             GenericConverterTestHelper<DateTime>("DateTimeConverter", new DateTime(2018, 12, 3), "\"2018-12-03T00:00:00\"", options);
             GenericConverterTestHelper<DateTimeOffset>("DateTimeOffsetConverter", new DateTimeOffset(new DateTime(2018, 12, 3, 00, 00, 00, DateTimeKind.Utc)), "\"2018-12-03T00:00:00+00:00\"", options);
             Guid testGuid = new Guid();
-            GenericConverterTestHelper<Guid>("GuidConverter", testGuid, $"\"{testGuid.ToString()}\"", options);
-            GenericConverterTestHelper<KeyValuePair<string, string>>("KeyValuePairConverter`2", new KeyValuePair<string, string>("key", "value"), @"{""Key"":""key"",""Value"":""value""}", options);
+            GenericConverterTestHelper<Guid>("GuidConverter", testGuid, $"\"{testGuid}\"", options);
             GenericConverterTestHelper<Uri>("UriConverter", new Uri("http://test.com"), "\"http://test.com\"", options);
         }
 
         [Fact]
+        public static void Options_GetConverter_GivesCorrectKeyValuePairConverter()
+        {
+            GenericConverterTestHelper<KeyValuePair<string, string>>(
+                converterName: "KeyValuePairConverter`2",
+                objectValue: new KeyValuePair<string, string>("key", "value"),
+                stringValue: @"{""Key"":""key"",""Value"":""value""}",
+                options: new JsonSerializerOptions(),
+                nullOptionOkay: false);
+        }
+
+        [Fact]
         public static void Options_GetConverter_GivesCorrectCustomConverterAndReadWriteSuccess()
         {
             var options = new JsonSerializerOptions();
@@ -416,7 +426,7 @@ namespace System.Text.Json.Serialization.Tests
             GenericConverterTestHelper<long[]>("LongArrayConverter", new long[] { 1, 2, 3, 4 }, "\"1,2,3,4\"", options);
         }
 
-        private static void GenericConverterTestHelper<T>(string converterName, object objectValue, string stringValue, JsonSerializerOptions options)
+        private static void GenericConverterTestHelper<T>(string converterName, object objectValue, string stringValue, JsonSerializerOptions options, bool nullOptionOkay = true)
         {
             JsonConverter<T> converter = (JsonConverter<T>)options.GetConverter(typeof(T));
 
@@ -427,7 +437,7 @@ namespace System.Text.Json.Serialization.Tests
             Utf8JsonReader reader = new Utf8JsonReader(data);
             reader.Read();
 
-            T valueRead = converter.Read(ref reader, typeof(T), null); // Test with null option.
+            T valueRead = converter.Read(ref reader, typeof(T), nullOptionOkay ? null: options);
             Assert.Equal(objectValue, valueRead);
 
             if (reader.TokenType != JsonTokenType.EndObject)
@@ -444,7 +454,7 @@ namespace System.Text.Json.Serialization.Tests
                 Assert.Equal(stringValue, Encoding.UTF8.GetString(stream.ToArray()));
 
                 writer.Reset(stream);
-                converter.Write(writer, (T)objectValue, null); // Test with null option.
+                converter.Write(writer, (T)objectValue, nullOptionOkay ? null : options);
                 writer.Flush();
                 Assert.Equal(stringValue + stringValue, Encoding.UTF8.GetString(stream.ToArray()));
             }
@@ -538,30 +548,6 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Throws<ArgumentOutOfRangeException>(() => new JsonSerializerOptions(outOfRangeSerializerDefaults));
         }
 
-        private static JsonSerializerOptions CreateOptionsInstance()
-        {
-            var options = new JsonSerializerOptions
-            {
-                AllowTrailingCommas = true,
-                DefaultBufferSize = 20,
-                DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
-                Encoder = JavaScriptEncoder.Default,
-                IgnoreNullValues = true,
-                IgnoreReadOnlyProperties = true,
-                MaxDepth = 32,
-                PropertyNameCaseInsensitive = true,
-                PropertyNamingPolicy = new SimpleSnakeCasePolicy(),
-                ReadCommentHandling = JsonCommentHandling.Disallow,
-                ReferenceHandling = ReferenceHandling.Default,
-                WriteIndented = true,
-            };
-
-            options.Converters.Add(new JsonStringEnumConverter());
-            options.Converters.Add(new ConverterForInt32());
-
-            return options;
-        }
-
         private static JsonSerializerOptions GetFullyPopulatedOptionsInstance()
         {
             var options = new JsonSerializerOptions();
@@ -703,5 +689,39 @@ namespace System.Text.Json.Serialization.Tests
         {
             Assert.Throws<ArgumentException>(() => new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.Always });
         }
+
+        [Fact]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/36605")]
+        public static void ConverterRead_VerifyInvalidTypeToConvertFails()
+        {
+            var options = new JsonSerializerOptions();
+            Type typeToConvert = typeof(KeyValuePair<int, int>);
+            byte[] bytes = Encoding.UTF8.GetBytes(@"{""Key"":1,""Value"":2}");
+
+            JsonConverter<KeyValuePair<int, int>> converter =
+                (JsonConverter<KeyValuePair<int, int>>)options.GetConverter(typeToConvert);
+
+            // Baseline
+            var reader = new Utf8JsonReader(bytes);
+            reader.Read();
+            KeyValuePair<int, int> kvp = converter.Read(ref reader, typeToConvert, options);
+            Assert.Equal(1, kvp.Key);
+            Assert.Equal(2, kvp.Value);
+
+            // Test
+            reader = new Utf8JsonReader(bytes);
+            reader.Read();
+            try
+            {
+                converter.Read(ref reader, typeof(Dictionary<string, int>), options);
+            }
+            catch (Exception ex)
+            {
+                if (!(ex is InvalidOperationException))
+                {
+                    throw ex;
+                }
+            }
+        }
     }
 }
index 8ea7c33..0cd7c17 100644 (file)
@@ -21,8 +21,8 @@ namespace System.Text.Json.Serialization.Tests
         public static async Task HandleCollectionsAsync()
         {
             await RunTest<string>();
-            await RunTest<ClassWithString>();
-            await RunTest<ImmutableStructWithString>();
+            await RunTest<ClassWithKVP>();
+            await RunTest<ImmutableStructWithStrings>();
         }
 
         private static async Task RunTest<TElement>()
@@ -127,6 +127,11 @@ namespace System.Text.Json.Serialization.Tests
             {
                 return ImmutableDictionary.CreateRange(GetDict_TypedElements<TElement>(stringLength));
             }
+            else if (type == typeof(KeyValuePair<TElement, TElement>))
+            {
+                TElement item = GetCollectionElement<TElement>(stringLength);
+                return new KeyValuePair<TElement, TElement>(item, item);
+            }
             else if (
                 typeof(IDictionary<string, TElement>).IsAssignableFrom(type) ||
                 typeof(IReadOnlyDictionary<string, TElement>).IsAssignableFrom(type) ||
@@ -168,7 +173,7 @@ namespace System.Text.Json.Serialization.Tests
             }
         }
 
-        private static string GetPayloadWithWhiteSpace(string json) => json.Replace(" ", new string(' ', 4));
+        private static string GetPayloadWithWhiteSpace(string json) => json.Replace("  ", new string(' ', 8));
 
         private const int NumElements = 15;
 
@@ -237,21 +242,22 @@ namespace System.Text.Json.Serialization.Tests
             char randomChar = (char)rand.Next('a', 'z');
 
             string value = new string(randomChar, stringLength);
+            var kvp = new KeyValuePair<string, SimpleStruct>(value, new SimpleStruct {
+                One = 1,
+                Two = 2
+            });
 
             if (type == typeof(string))
             {
                 return (TElement)(object)value;
             }
-            else if (type == typeof(ClassWithString))
+            else if (type == typeof(ClassWithKVP))
             {
-                return (TElement)(object)new ClassWithString
-                {
-                    MyFirstString = value
-                };
+                return (TElement)(object)new ClassWithKVP { MyKvp = kvp };
             }
             else
             {
-                return (TElement)(object)new ImmutableStructWithString(value, value);
+                return (TElement)(object)new ImmutableStructWithStrings(value, value);
             }
 
             throw new NotImplementedException();
@@ -284,6 +290,10 @@ namespace System.Text.Json.Serialization.Tests
             {
                 yield return type;
             }
+            foreach (Type type in ObjectNotationTypes<TElement>())
+            {
+                yield return type;
+            }
             // Stack types
             foreach (Type type in StackTypes<TElement>())
             {
@@ -312,6 +322,11 @@ namespace System.Text.Json.Serialization.Tests
             yield return typeof(Queue<TElement>); // QueueOfTConverter
         }
 
+        private static IEnumerable<Type> ObjectNotationTypes<TElement>()
+        {
+            yield return typeof(KeyValuePair<TElement, TElement>); // KeyValuePairConverter
+        }
+
         private static IEnumerable<Type> DictionaryTypes<TElement>()
         {
             yield return typeof(Dictionary<string, TElement>); // DictionaryOfStringTValueConverter
@@ -337,18 +352,19 @@ namespace System.Text.Json.Serialization.Tests
             typeof(GenericIReadOnlyDictionaryWrapper<string, TElement>)
         };
 
-        private class ClassWithString
+        private class ClassWithKVP
         {
-            public string MyFirstString { get; set; }
+            public KeyValuePair<string, SimpleStruct> MyKvp { get; set; }
         }
 
-        private struct ImmutableStructWithString
+        private struct ImmutableStructWithStrings
         {
             public string MyFirstString { get; }
             public string MySecondString { get; }
 
             [JsonConstructor]
-            public ImmutableStructWithString(string myFirstString, string mySecondString)
+            public ImmutableStructWithStrings(
+                string myFirstString, string mySecondString)
             {
                 MyFirstString = myFirstString;
                 MySecondString = mySecondString;
@@ -391,6 +407,11 @@ namespace System.Text.Json.Serialization.Tests
             {
                 Assert.Equal("{}", JsonSerializer.Serialize(GetEmptyCollection<int>(type)));
             }
+
+            foreach (Type type in ObjectNotationTypes<int>())
+            {
+                Assert.Equal(@"{""Key"":0,""Value"":0}", JsonSerializer.Serialize(GetEmptyCollection<int>(type)));
+            }
         }
     }
 }