Add support for multiple non-string TKey on dictionaries (#38056)
authorDavid Cantu <dacantu@microsoft.com>
Tue, 7 Jul 2020 02:10:17 +0000 (19:10 -0700)
committerGitHub <noreply@github.com>
Tue, 7 Jul 2020 02:10:17 +0000 (19:10 -0700)
* Add support for multiple non-string TKey on dictionaries

* Fix build errors

* Fix performance regression

* Add Read/WriteWithQuotes to the remaining supported types

* Add support for a few more missing types

* Add policy support for Enum keys

* Address performance regression.

* Fix test error on netstandard

* 1. Add support for escaped characters on Read and avoid escaping on Write
2. Address some feedback

* Remove TryGet*Core methods to mitigate perf regression

* Remove duplicated escaping tests

* Cache TKey and TValue converters

* Remove duplicated code used to read the dictionary key

* Bring back TryGet*Core

* Fix test errors with floating point types

* Address nits and feedback from steveharter

55 files changed:
src/libraries/System.Text.Json/src/Resources/Strings.resx
src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs
src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs
src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.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/DictionaryOfTKeyTValueConverter.cs [moved from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfStringTValueConverter.cs with 66% similarity]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfTKeyTValueConverter.cs [moved from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryOfStringTValueConverter.cs with 76% similarity]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverterFactory.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfTKeyTValueConverter.cs [moved from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IReadOnlyDictionaryOfStringTValueConverter.cs with 65% similarity]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfTKeyTValueConverter.cs [moved from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ImmutableDictionaryOfStringTValueConverter.cs with 70% similarity]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/BooleanConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeOffsetConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/GuidConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ObjectConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/StringConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs
src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs
src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTime.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.DateTimeOffset.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Decimal.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Double.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Float.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Guid.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.Literal.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.SignedNumber.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.String.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.UnsignedNumber.cs
src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Dictionary.NonStringKey.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Dictionary.cs
src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Generic.Read.cs
src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests/CustomConverterTests.DerivedTypes.cs
src/libraries/System.Text.Json/tests/Serialization/ExtensionDataTests.cs
src/libraries/System.Text.Json/tests/Serialization/PropertyVisibilityTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj

index 1a029bf..7309f57 100644 (file)
   <data name="DefaultIgnoreConditionInvalid" xml:space="preserve">
     <value>The value cannot be 'JsonIgnoreCondition.Always'.</value>
   </data>
+  <data name="FormatBoolean" xml:space="preserve">
+    <value>The JSON value is not in a supported Boolean format.</value>
+  </data>
+  <data name="DictionaryKeyTypeNotSupported" xml:space="preserve">
+    <value>The type '{0}' is not a supported Dictionary key type.</value>
+  </data>
 </root>
index f09a442..5d8e706 100644 (file)
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\ConcurrentQueueOfTConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\ConcurrentStackOfTConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\DictionaryDefaultConverter.cs" />
-    <Compile Include="System\Text\Json\Serialization\Converters\Collection\DictionaryOfStringTValueConverter.cs" />
+    <Compile Include="System\Text\Json\Serialization\Converters\Collection\DictionaryOfTKeyTValueConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\ICollectionOfTConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\IDictionaryConverter.cs" />
-    <Compile Include="System\Text\Json\Serialization\Converters\Collection\IDictionaryOfStringTValueConverter.cs" />
+    <Compile Include="System\Text\Json\Serialization\Converters\Collection\IDictionaryOfTKeyTValueConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\IEnumerableConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\IEnumerableConverterFactory.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\IEnumerableConverterFactoryHelpers.cs" />
@@ -78,9 +78,9 @@
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\IEnumerableWithAddMethodConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\IListConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\IListOfTConverter.cs" />
-    <Compile Include="System\Text\Json\Serialization\Converters\Collection\ImmutableDictionaryOfStringTValueConverter.cs" />
+    <Compile Include="System\Text\Json\Serialization\Converters\Collection\ImmutableDictionaryOfTKeyTValueConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\ImmutableEnumerableOfTConverter.cs" />
-    <Compile Include="System\Text\Json\Serialization\Converters\Collection\IReadOnlyDictionaryOfStringTValueConverter.cs" />
+    <Compile Include="System\Text\Json\Serialization\Converters\Collection\IReadOnlyDictionaryOfTKeyTValueConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\ISetOfTConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\JsonCollectionConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\JsonDictionaryConverter.cs" />
index 1d13e73..0e695e0 100644 (file)
@@ -66,6 +66,7 @@ namespace System.Text.Json
         public const int MaxBase64ValueTokenSize = (MaxEscapedTokenSize >> 2) * 3 / MaxExpansionFactorWhileEscaping;  // 125_000_000 bytes
         public const int MaxCharacterTokenSize = MaxEscapedTokenSize / MaxExpansionFactorWhileEscaping; // 166_666_666 characters
 
+        public const int MaximumFormatBooleanLength = 5;
         public const int MaximumFormatInt64Length = 20;   // 19 + sign (i.e. -9223372036854775808)
         public const int MaximumFormatUInt64Length = 20;  // i.e. 18446744073709551615
         public const int MaximumFormatDoubleLength = 128;  // default (i.e. 'G'), using 128 (rather than say 32) to be future-proof.
index 642d4a6..a81a971 100644 (file)
@@ -6,6 +6,7 @@ using System.Buffers;
 using System.Buffers.Text;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
 
 namespace System.Text.Json
 {
@@ -138,6 +139,16 @@ namespace System.Text.Json
             return value;
         }
 
+        internal byte GetByteWithQuotes()
+        {
+            ReadOnlySpan<byte> span = GetUnescapedSpan();
+            if (!TryGetByteCore(out byte value, span))
+            {
+                throw ThrowHelper.GetFormatException(NumericType.Byte);
+            }
+            return value;
+        }
+
         /// <summary>
         /// Parses the current JSON token value from the source as an <see cref="sbyte"/>.
         /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to an <see cref="sbyte"/>
@@ -163,6 +174,16 @@ namespace System.Text.Json
             return value;
         }
 
+        internal sbyte GetSByteWithQuotes()
+        {
+            ReadOnlySpan<byte> span = GetUnescapedSpan();
+            if (!TryGetSByteCore(out sbyte value, span))
+            {
+                throw ThrowHelper.GetFormatException(NumericType.SByte);
+            }
+            return value;
+        }
+
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="short"/>.
         /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a <see cref="short"/>
@@ -187,6 +208,16 @@ namespace System.Text.Json
             return value;
         }
 
+        internal short GetInt16WithQuotes()
+        {
+            ReadOnlySpan<byte> span = GetUnescapedSpan();
+            if (!TryGetInt16Core(out short value, span))
+            {
+                throw ThrowHelper.GetFormatException(NumericType.Int16);
+            }
+            return value;
+        }
+
         /// <summary>
         /// Parses the current JSON token value from the source as an <see cref="int"/>.
         /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to an <see cref="int"/>
@@ -211,6 +242,16 @@ namespace System.Text.Json
             return value;
         }
 
+        internal int GetInt32WithQuotes()
+        {
+            ReadOnlySpan<byte> span = GetUnescapedSpan();
+            if (!TryGetInt32Core(out int value, span))
+            {
+                throw ThrowHelper.GetFormatException(NumericType.Int32);
+            }
+            return value;
+        }
+
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="long"/>.
         /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a <see cref="long"/>
@@ -235,6 +276,16 @@ namespace System.Text.Json
             return value;
         }
 
+        internal long GetInt64WithQuotes()
+        {
+            ReadOnlySpan<byte> span = GetUnescapedSpan();
+            if (!TryGetInt64Core(out long value, span))
+            {
+                throw ThrowHelper.GetFormatException(NumericType.Int64);
+            }
+            return value;
+        }
+
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="ushort"/>.
         /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a <see cref="ushort"/>
@@ -260,6 +311,16 @@ namespace System.Text.Json
             return value;
         }
 
+        internal ushort GetUInt16WithQuotes()
+        {
+            ReadOnlySpan<byte> span = GetUnescapedSpan();
+            if (!TryGetUInt16Core(out ushort value, span))
+            {
+                throw ThrowHelper.GetFormatException(NumericType.UInt16);
+            }
+            return value;
+        }
+
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="uint"/>.
         /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a <see cref="uint"/>
@@ -285,6 +346,16 @@ namespace System.Text.Json
             return value;
         }
 
+        internal uint GetUInt32WithQuotes()
+        {
+            ReadOnlySpan<byte> span = GetUnescapedSpan();
+            if (!TryGetUInt32Core(out uint value, span))
+            {
+                throw ThrowHelper.GetFormatException(NumericType.UInt32);
+            }
+            return value;
+        }
+
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="ulong"/>.
         /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a <see cref="ulong"/>
@@ -310,6 +381,16 @@ namespace System.Text.Json
             return value;
         }
 
+        internal ulong GetUInt64WithQuotes()
+        {
+            ReadOnlySpan<byte> span = GetUnescapedSpan();
+            if (!TryGetUInt64Core(out ulong value, span))
+            {
+                throw ThrowHelper.GetFormatException(NumericType.UInt64);
+            }
+            return value;
+        }
+
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="float"/>.
         /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a <see cref="float"/>
@@ -333,6 +414,16 @@ namespace System.Text.Json
             return value;
         }
 
+        internal float GetSingleWithQuotes()
+        {
+            ReadOnlySpan<byte> span = GetUnescapedSpan();
+            if (!TryGetSingleCore(out float value, span))
+            {
+                throw ThrowHelper.GetFormatException(NumericType.Single);
+            }
+            return value;
+        }
+
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="double"/>.
         /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a <see cref="double"/>
@@ -356,6 +447,16 @@ namespace System.Text.Json
             return value;
         }
 
+        internal double GetDoubleWithQuotes()
+        {
+            ReadOnlySpan<byte> span = GetUnescapedSpan();
+            if (!TryGetDoubleCore(out double value, span))
+            {
+                throw ThrowHelper.GetFormatException(NumericType.Double);
+            }
+            return value;
+        }
+
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="decimal"/>.
         /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a <see cref="decimal"/>
@@ -379,6 +480,16 @@ namespace System.Text.Json
             return value;
         }
 
+        internal decimal GetDecimalWithQuotes()
+        {
+            ReadOnlySpan<byte> span = GetUnescapedSpan();
+            if (!TryGetDecimalCore(out decimal value, span))
+            {
+                throw ThrowHelper.GetFormatException(NumericType.Decimal);
+            }
+            return value;
+        }
+
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="DateTime"/>.
         /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a <see cref="DateTime"/>
@@ -402,6 +513,16 @@ namespace System.Text.Json
             return value;
         }
 
+        internal DateTime GetDateTimeNoValidation()
+        {
+            if (!TryGetDateTimeCore(out DateTime value))
+            {
+                throw ThrowHelper.GetFormatException(DataType.DateTime);
+            }
+
+            return value;
+        }
+
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="DateTimeOffset"/>.
         /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a <see cref="DateTimeOffset"/>
@@ -425,6 +546,16 @@ namespace System.Text.Json
             return value;
         }
 
+        internal DateTimeOffset GetDateTimeOffsetNoValidation()
+        {
+            if (!TryGetDateTimeOffsetCore(out DateTimeOffset value))
+            {
+                throw ThrowHelper.GetFormatException(DataType.DateTimeOffset);
+            }
+
+            return value;
+        }
+
         /// <summary>
         /// Parses the current JSON token value from the source as a <see cref="Guid"/>.
         /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a <see cref="Guid"/>
@@ -448,6 +579,16 @@ namespace System.Text.Json
             return value;
         }
 
+        internal Guid GetGuidNoValidation()
+        {
+            if (!TryGetGuidCore(out Guid value))
+            {
+                throw ThrowHelper.GetFormatException(DataType.Guid);
+            }
+
+            return value;
+        }
+
         /// <summary>
         /// Parses the current JSON token value from the source and decodes the Base64 encoded JSON string as bytes.
         /// Returns <see langword="true"/> if the entire token value is encoded as valid Base64 text and can be successfully
@@ -496,6 +637,12 @@ namespace System.Text.Json
             }
 
             ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
+            return TryGetByteCore(out value, span);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal bool TryGetByteCore(out byte value, ReadOnlySpan<byte> span)
+        {
             if (Utf8Parser.TryParse(span, out byte tmp, out int bytesConsumed)
                 && span.Length == bytesConsumed)
             {
@@ -526,6 +673,12 @@ namespace System.Text.Json
             }
 
             ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
+            return TryGetSByteCore(out value, span);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal bool TryGetSByteCore(out sbyte value, ReadOnlySpan<byte> span)
+        {
             if (Utf8Parser.TryParse(span, out sbyte tmp, out int bytesConsumed)
                 && span.Length == bytesConsumed)
             {
@@ -555,6 +708,12 @@ namespace System.Text.Json
             }
 
             ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
+            return TryGetInt16Core(out value, span);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal bool TryGetInt16Core(out short value, ReadOnlySpan<byte> span)
+        {
             if (Utf8Parser.TryParse(span, out short tmp, out int bytesConsumed)
                 && span.Length == bytesConsumed)
             {
@@ -584,6 +743,12 @@ namespace System.Text.Json
             }
 
             ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
+            return TryGetInt32Core(out value, span);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal bool TryGetInt32Core(out int value, ReadOnlySpan<byte> span)
+        {
             if (Utf8Parser.TryParse(span, out int tmp, out int bytesConsumed)
                 && span.Length == bytesConsumed)
             {
@@ -613,6 +778,12 @@ namespace System.Text.Json
             }
 
             ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
+            return TryGetInt64Core(out value, span);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal bool TryGetInt64Core(out long value, ReadOnlySpan<byte> span)
+        {
             if (Utf8Parser.TryParse(span, out long tmp, out int bytesConsumed)
                 && span.Length == bytesConsumed)
             {
@@ -643,6 +814,12 @@ namespace System.Text.Json
             }
 
             ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
+            return TryGetUInt16Core(out value, span);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal bool TryGetUInt16Core(out ushort value, ReadOnlySpan<byte> span)
+        {
             if (Utf8Parser.TryParse(span, out ushort tmp, out int bytesConsumed)
                 && span.Length == bytesConsumed)
             {
@@ -673,6 +850,12 @@ namespace System.Text.Json
             }
 
             ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
+            return TryGetUInt32Core(out value, span);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal bool TryGetUInt32Core(out uint value, ReadOnlySpan<byte> span)
+        {
             if (Utf8Parser.TryParse(span, out uint tmp, out int bytesConsumed)
                 && span.Length == bytesConsumed)
             {
@@ -703,6 +886,12 @@ namespace System.Text.Json
             }
 
             ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
+            return TryGetUInt64Core(out value, span);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal bool TryGetUInt64Core(out ulong value, ReadOnlySpan<byte> span)
+        {
             if (Utf8Parser.TryParse(span, out ulong tmp, out int bytesConsumed)
                 && span.Length == bytesConsumed)
             {
@@ -732,6 +921,12 @@ namespace System.Text.Json
             }
 
             ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
+            return TryGetSingleCore(out value, span);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal bool TryGetSingleCore(out float value, ReadOnlySpan<byte> span)
+        {
             if (Utf8Parser.TryParse(span, out float tmp, out int bytesConsumed, _numberFormat)
                 && span.Length == bytesConsumed)
             {
@@ -761,6 +956,12 @@ namespace System.Text.Json
             }
 
             ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
+            return TryGetDoubleCore(out value, span);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal bool TryGetDoubleCore(out double value, ReadOnlySpan<byte> span)
+        {
             if (Utf8Parser.TryParse(span, out double tmp, out int bytesConsumed, _numberFormat)
                 && span.Length == bytesConsumed)
             {
@@ -790,6 +991,12 @@ namespace System.Text.Json
             }
 
             ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
+            return TryGetDecimalCore(out value, span);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal bool TryGetDecimalCore(out decimal value, ReadOnlySpan<byte> span)
+        {
             if (Utf8Parser.TryParse(span, out decimal tmp, out int bytesConsumed, _numberFormat)
                 && span.Length == bytesConsumed)
             {
@@ -818,6 +1025,11 @@ namespace System.Text.Json
                 throw ThrowHelper.GetInvalidOperationException_ExpectedString(TokenType);
             }
 
+            return TryGetDateTimeCore(out value);
+        }
+
+        internal bool TryGetDateTimeCore(out DateTime value)
+        {
             ReadOnlySpan<byte> span = stackalloc byte[0];
 
             if (HasValueSequence)
@@ -881,6 +1093,11 @@ namespace System.Text.Json
                 throw ThrowHelper.GetInvalidOperationException_ExpectedString(TokenType);
             }
 
+            return TryGetDateTimeOffsetCore(out value);
+        }
+
+        internal bool TryGetDateTimeOffsetCore(out DateTimeOffset value)
+        {
             ReadOnlySpan<byte> span = stackalloc byte[0];
 
             if (HasValueSequence)
@@ -945,6 +1162,11 @@ namespace System.Text.Json
                 throw ThrowHelper.GetInvalidOperationException_ExpectedString(TokenType);
             }
 
+            return TryGetGuidCore(out value);
+        }
+
+        internal bool TryGetGuidCore(out Guid value)
+        {
             ReadOnlySpan<byte> span = stackalloc byte[0];
 
             if (HasValueSequence)
index cb43f7b..b9d2723 100644 (file)
@@ -2552,5 +2552,18 @@ namespace System.Text.Json
                 JsonTokenType.True => nameof(JsonTokenType.True),
                 _ => ((byte)TokenType).ToString()
             };
+
+        private ReadOnlySpan<byte> GetUnescapedSpan()
+        {
+            ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
+            if (_stringHasEscaping)
+            {
+                int idx = span.IndexOf(JsonConstants.BackSlash);
+                Debug.Assert(idx != -1);
+                span = JsonReaderHelper.GetUnescapedSpan(span, idx);
+            }
+
+            return span;
+        }
     }
 }
index 1c4b0fa..afd541c 100644 (file)
@@ -10,13 +10,14 @@ namespace System.Text.Json.Serialization.Converters
     /// <summary>
     /// Default base class implementation of <cref>JsonDictionaryConverter{TCollection}</cref> .
     /// </summary>
-    internal abstract class DictionaryDefaultConverter<TCollection, TValue>
+    internal abstract class DictionaryDefaultConverter<TCollection, TKey, TValue>
         : JsonDictionaryConverter<TCollection>
+        where TKey : notnull
     {
         /// <summary>
         /// When overridden, adds the value to the collection.
         /// </summary>
-        protected abstract void Add(in TValue value, JsonSerializerOptions options, ref ReadStack state);
+        protected abstract void Add(TKey key, in TValue value, JsonSerializerOptions options, ref ReadStack state);
 
         /// <summary>
         /// When overridden, converts the temporary collection held in state.Current.ReturnValue to the final collection.
@@ -31,37 +32,25 @@ namespace System.Text.Json.Serialization.Converters
 
         internal override Type ElementType => typeof(TValue);
 
-        protected static JsonConverter<TValue> GetElementConverter(ref ReadStack state)
-        {
-            JsonConverter<TValue> converter = (JsonConverter<TValue>)state.Current.JsonClassInfo.ElementClassInfo!.PropertyInfoForClassInfo.ConverterBase;
-            Debug.Assert(converter != null); // It should not be possible to have a null converter at this point.
-
-            return converter;
-        }
-
-        protected string GetKeyName(string key, ref WriteStack state, JsonSerializerOptions options)
-        {
-            if (options.DictionaryKeyPolicy != null && !state.Current.IgnoreDictionaryKeyPolicy)
-            {
-                key = options.DictionaryKeyPolicy.ConvertName(key);
-
-                if (key == null)
-                {
-                    ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(options.DictionaryKeyPolicy);
-                }
-            }
+        protected Type KeyType = typeof(TKey);
+        // For string keys we don't use a key converter
+        // in order to avoid performance regression on already supported types.
+        protected bool IsStringKey = typeof(TKey) == typeof(string);
 
-            return key;
-        }
+        protected JsonConverter<TKey>? _keyConverter;
+        protected JsonConverter<TValue>? _valueConverter;
 
-        protected static JsonConverter<TValue> GetValueConverter(ref WriteStack state)
+        protected static JsonConverter<TValue> GetValueConverter(JsonClassInfo classInfo)
         {
-            JsonConverter<TValue> converter = (JsonConverter<TValue>)state.Current.DeclaredJsonPropertyInfo!.ConverterBase;
+            JsonConverter<TValue> converter = (JsonConverter<TValue>)classInfo.ElementClassInfo!.PropertyInfoForClassInfo.ConverterBase;
             Debug.Assert(converter != null); // It should not be possible to have a null converter at this point.
 
             return converter;
         }
 
+        protected static JsonConverter<TKey> GetKeyConverter(Type keyType, JsonSerializerOptions options)
+            => (JsonConverter<TKey>)options.GetDictionaryKeyConverter(keyType);
+
         internal sealed override bool OnTryRead(
             ref Utf8JsonReader reader,
             Type typeToConvert,
@@ -80,8 +69,8 @@ namespace System.Text.Json.Serialization.Converters
 
                 CreateCollection(ref reader, ref state);
 
-                JsonConverter<TValue> elementConverter = GetElementConverter(ref state);
-                if (elementConverter.CanUseDirectReadOrWrite)
+                JsonConverter<TValue> valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo);
+                if (valueConverter.CanUseDirectReadOrWrite)
                 {
                     // Process all elements.
                     while (true)
@@ -97,12 +86,12 @@ namespace System.Text.Json.Serialization.Converters
                         // Read method would have thrown if otherwise.
                         Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
 
-                        state.Current.JsonPropertyNameAsString = reader.GetString();
+                        TKey key = ReadDictionaryKey(ref reader, ref state);
 
                         // Read the value and add.
                         reader.ReadWithVerify();
-                        TValue element = elementConverter.Read(ref reader, typeof(TValue), options);
-                        Add(element!, options, ref state);
+                        TValue element = valueConverter.Read(ref reader, typeof(TValue), options);
+                        Add(key, element!, options, ref state);
                     }
                 }
                 else
@@ -121,13 +110,13 @@ namespace System.Text.Json.Serialization.Converters
                         // Read method would have thrown if otherwise.
                         Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
 
-                        state.Current.JsonPropertyNameAsString = reader.GetString();
+                        TKey key = ReadDictionaryKey(ref reader, ref state);
 
                         reader.ReadWithVerify();
 
                         // Get the value from the converter and add it.
-                        elementConverter.TryRead(ref reader, typeof(TValue), options, ref state, out TValue element);
-                        Add(element!, options, ref state);
+                        valueConverter.TryRead(ref reader, typeof(TValue), options, ref state, out TValue element);
+                        Add(key, element!, options, ref state);
                     }
                 }
             }
@@ -184,7 +173,7 @@ namespace System.Text.Json.Serialization.Converters
                 }
 
                 // Process all elements.
-                JsonConverter<TValue> elementConverter = GetElementConverter(ref state);
+                JsonConverter<TValue> elementConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo);
                 while (true)
                 {
                     if (state.Current.PropertyState == StackFramePropertyState.None)
@@ -212,7 +201,6 @@ namespace System.Text.Json.Serialization.Converters
 
                         state.Current.PropertyState = StackFramePropertyState.Name;
 
-                        // Verify property doesn't contain metadata.
                         if (preserveReferences)
                         {
                             ReadOnlySpan<byte> propertyName = reader.GetSpan();
@@ -222,7 +210,7 @@ namespace System.Text.Json.Serialization.Converters
                             }
                         }
 
-                        state.Current.JsonPropertyNameAsString = reader.GetString();
+                        state.Current.DictionaryKey = ReadDictionaryKey(ref reader, ref state);
                     }
 
                     if (state.Current.PropertyState < StackFramePropertyState.ReadValue)
@@ -246,7 +234,8 @@ namespace System.Text.Json.Serialization.Converters
                             return false;
                         }
 
-                        Add(element!, options, ref state);
+                        TKey key = (TKey)state.Current.DictionaryKey!;
+                        Add(key, element!, options, ref state);
                         state.Current.EndElement();
                     }
                 }
@@ -255,6 +244,29 @@ namespace System.Text.Json.Serialization.Converters
             ConvertCollection(ref state, options);
             value = (TCollection)state.Current.ReturnValue!;
             return true;
+
+            TKey ReadDictionaryKey(ref Utf8JsonReader reader, ref ReadStack state)
+            {
+                TKey key;
+                string unescapedPropertyNameAsString;
+
+                // Special case string to avoid calling GetString twice and save one allocation.
+                if (IsStringKey)
+                {
+                    unescapedPropertyNameAsString = reader.GetString()!;
+                    key = (TKey)(object)unescapedPropertyNameAsString;
+                }
+                else
+                {
+                    JsonConverter<TKey> keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options);
+                    key = keyConverter.ReadWithQuotes(ref reader);
+                    unescapedPropertyNameAsString = reader.GetString()!;
+                }
+
+                // Copy key name for JSON Path support in case of error.
+                state.Current.JsonPropertyNameAsString = unescapedPropertyNameAsString;
+                return key;
+            }
         }
 
         internal sealed override bool OnTryWrite(
@@ -10,13 +10,13 @@ namespace System.Text.Json.Serialization.Converters
     /// Converter for Dictionary{string, TValue} that (de)serializes as a JSON object with properties
     /// representing the dictionary element key and value.
     /// </summary>
-    internal sealed class DictionaryOfStringTValueConverter<TCollection, TValue>
-        : DictionaryDefaultConverter<TCollection, TValue>
-        where TCollection : Dictionary<string, TValue>
+    internal sealed class DictionaryOfTKeyTValueConverter<TCollection, TKey, TValue>
+        : DictionaryDefaultConverter<TCollection, TKey, TValue>
+        where TCollection : Dictionary<TKey, TValue>
+        where TKey : notnull
     {
-        protected override void Add(in TValue value, JsonSerializerOptions options, ref ReadStack state)
+        protected override void Add(TKey key, in TValue value, JsonSerializerOptions options, ref ReadStack state)
         {
-            string key = state.Current.JsonPropertyNameAsString!;
             ((TCollection)state.Current.ReturnValue!)[key] = value;
         }
 
@@ -36,7 +36,7 @@ namespace System.Text.Json.Serialization.Converters
             JsonSerializerOptions options,
             ref WriteStack state)
         {
-            Dictionary<string, TValue>.Enumerator enumerator;
+            Dictionary<TKey, TValue>.Enumerator enumerator;
             if (state.Current.CollectionEnumerator == null)
             {
                 enumerator = value.GetEnumerator();
@@ -47,18 +47,20 @@ namespace System.Text.Json.Serialization.Converters
             }
             else
             {
-                enumerator = (Dictionary<string, TValue>.Enumerator)state.Current.CollectionEnumerator;
+                enumerator = (Dictionary<TKey, TValue>.Enumerator)state.Current.CollectionEnumerator;
             }
 
-            JsonConverter<TValue> converter = GetValueConverter(ref state);
-            if (!state.SupportContinuation && converter.CanUseDirectReadOrWrite)
+            JsonConverter<TKey> keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options);
+            JsonConverter<TValue> valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo);
+            if (!state.SupportContinuation && valueConverter.CanUseDirectReadOrWrite)
             {
                 // Fast path that avoids validation and extra indirection.
                 do
                 {
-                    string key = GetKeyName(enumerator.Current.Key, ref state, options);
-                    writer.WritePropertyName(key);
-                    converter.Write(writer, enumerator.Current.Value, options);
+                    TKey key = enumerator.Current.Key;
+                    keyConverter.WriteWithQuotes(writer, key, options, ref state);
+
+                    valueConverter.Write(writer, enumerator.Current.Value, options);
                 } while (enumerator.MoveNext());
             }
             else
@@ -74,12 +76,13 @@ namespace System.Text.Json.Serialization.Converters
                     if (state.Current.PropertyState < StackFramePropertyState.Name)
                     {
                         state.Current.PropertyState = StackFramePropertyState.Name;
-                        string key = GetKeyName(enumerator.Current.Key, ref state, options);
-                        writer.WritePropertyName(key);
+
+                        TKey key = enumerator.Current.Key;
+                        keyConverter.WriteWithQuotes(writer, key, options, ref state);
                     }
 
                     TValue element = enumerator.Current.Value;
-                    if (!converter.TryWrite(writer, element, options, ref state))
+                    if (!valueConverter.TryWrite(writer, element, options, ref state))
                     {
                         state.Current.CollectionEnumerator = enumerator;
                         return false;
index 4927187..1e586c0 100644 (file)
@@ -12,15 +12,19 @@ namespace System.Text.Json.Serialization.Converters
     /// representing the dictionary element key and value.
     /// </summary>
     internal sealed class IDictionaryConverter<TCollection>
-        : DictionaryDefaultConverter<TCollection, object?>
+        : DictionaryDefaultConverter<TCollection, string, object?>
         where TCollection : IDictionary
     {
-        protected override void Add(in object? value, JsonSerializerOptions options, ref ReadStack state)
+        protected override void Add(string key, in object? value, JsonSerializerOptions options, ref ReadStack state)
         {
-            string key = state.Current.JsonPropertyNameAsString!;
             ((IDictionary)state.Current.ReturnValue!)[key] = value;
         }
 
+        private JsonConverter<object>? _objectConverter;
+
+        private static JsonConverter<object> GetObjectKeyConverter(JsonSerializerOptions options)
+            => (JsonConverter<object>)options.GetDictionaryKeyConverter(typeof(object));
+
         protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state)
         {
             JsonClassInfo classInfo = state.Current.JsonClassInfo;
@@ -68,7 +72,7 @@ namespace System.Text.Json.Serialization.Converters
                 enumerator = (IDictionaryEnumerator)state.Current.CollectionEnumerator;
             }
 
-            JsonConverter<object?> converter = GetValueConverter(ref state);
+            JsonConverter<object?> valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo);
             do
             {
                 if (ShouldFlush(writer, ref state))
@@ -80,20 +84,24 @@ namespace System.Text.Json.Serialization.Converters
                 if (state.Current.PropertyState < StackFramePropertyState.Name)
                 {
                     state.Current.PropertyState = StackFramePropertyState.Name;
-
-                    if (enumerator.Key is string key)
+                    object key = enumerator.Key;
+                    // Optimize for string since that's the hot path.
+                    if (key is string keyString)
                     {
-                        key = GetKeyName(key, ref state, options);
-                        writer.WritePropertyName(key);
+                        JsonConverter<string> stringKeyConverter = _keyConverter ??= GetKeyConverter(KeyType, options);
+                        stringKeyConverter.WriteWithQuotes(writer, keyString, options, ref state);
                     }
                     else
                     {
-                        ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(state.Current.DeclaredJsonPropertyInfo!.RuntimePropertyType!);
+                        // IDictionary is a special case since it has polymorphic object semantics on serialization
+                        // but needs to use JsonConverter<string> on deserialization.
+                        JsonConverter<object> objectKeyConverter = _objectConverter ??= GetObjectKeyConverter(options);
+                        objectKeyConverter.WriteWithQuotes(writer, key, options, ref state);
                     }
                 }
 
                 object? element = enumerator.Value;
-                if (!converter.TryWrite(writer, element, options, ref state))
+                if (!valueConverter.TryWrite(writer, element, options, ref state))
                 {
                     state.Current.CollectionEnumerator = enumerator;
                     return false;
@@ -10,13 +10,13 @@ namespace System.Text.Json.Serialization.Converters
     /// Converter for <cref>System.Collections.Generic.IDictionary{string, TValue}</cref> that
     /// (de)serializes as a JSON object with properties representing the dictionary element key and value.
     /// </summary>
-    internal sealed class IDictionaryOfStringTValueConverter<TCollection, TValue>
-        : DictionaryDefaultConverter<TCollection, TValue>
-        where TCollection : IDictionary<string, TValue>
+    internal sealed class IDictionaryOfTKeyTValueConverter<TCollection, TKey, TValue>
+        : DictionaryDefaultConverter<TCollection, TKey, TValue>
+        where TCollection : IDictionary<TKey, TValue>
+        where TKey : notnull
     {
-        protected override void Add(in TValue value, JsonSerializerOptions options, ref ReadStack state)
+        protected override void Add(TKey key, in TValue value, JsonSerializerOptions options, ref ReadStack state)
         {
-            string key = state.Current.JsonPropertyNameAsString!;
             ((TCollection)state.Current.ReturnValue!)[key] = value;
         }
 
@@ -57,7 +57,7 @@ namespace System.Text.Json.Serialization.Converters
             JsonSerializerOptions options,
             ref WriteStack state)
         {
-            IEnumerator<KeyValuePair<string, TValue>> enumerator;
+            IEnumerator<KeyValuePair<TKey, TValue>> enumerator;
             if (state.Current.CollectionEnumerator == null)
             {
                 enumerator = value.GetEnumerator();
@@ -68,10 +68,11 @@ namespace System.Text.Json.Serialization.Converters
             }
             else
             {
-                enumerator = (IEnumerator<KeyValuePair<string, TValue>>)state.Current.CollectionEnumerator;
+                enumerator = (IEnumerator<KeyValuePair<TKey, TValue>>)state.Current.CollectionEnumerator;
             }
 
-            JsonConverter<TValue> converter = GetValueConverter(ref state);
+            JsonConverter<TKey> keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options);
+            JsonConverter<TValue> valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo);
             do
             {
                 if (ShouldFlush(writer, ref state))
@@ -83,12 +84,12 @@ namespace System.Text.Json.Serialization.Converters
                 if (state.Current.PropertyState < StackFramePropertyState.Name)
                 {
                     state.Current.PropertyState = StackFramePropertyState.Name;
-                    string key = GetKeyName(enumerator.Current.Key, ref state, options);
-                    writer.WritePropertyName(key);
+                    TKey key = enumerator.Current.Key;
+                    keyConverter.WriteWithQuotes(writer, key, options, ref state);
                 }
 
                 TValue element = enumerator.Current.Value;
-                if (!converter.TryWrite(writer, element, options, ref state))
+                if (!valueConverter.TryWrite(writer, element, options, ref state))
                 {
                     state.Current.CollectionEnumerator = enumerator;
                     return false;
@@ -106,7 +107,7 @@ namespace System.Text.Json.Serialization.Converters
             {
                 if (TypeToConvert.IsAbstract || TypeToConvert.IsInterface)
                 {
-                    return typeof(Dictionary<string, TValue>);
+                    return typeof(Dictionary<TKey, TValue>);
                 }
 
                 return TypeToConvert;
index 5601268..7b1cdd1 100644 (file)
@@ -28,25 +28,26 @@ namespace System.Text.Json.Serialization.Converters
         [DynamicDependency("#ctor", typeof(ArrayConverter<,>))]
         [DynamicDependency("#ctor", typeof(ConcurrentQueueOfTConverter<,>))]
         [DynamicDependency("#ctor", typeof(ConcurrentStackOfTConverter<,>))]
-        [DynamicDependency("#ctor", typeof(DictionaryOfStringTValueConverter<,>))]
+        [DynamicDependency("#ctor", typeof(DictionaryOfTKeyTValueConverter<,,>))]
         [DynamicDependency("#ctor", typeof(ICollectionOfTConverter<,>))]
-        [DynamicDependency("#ctor", typeof(IDictionaryOfStringTValueConverter<,>))]
+        [DynamicDependency("#ctor", typeof(IDictionaryOfTKeyTValueConverter<,,>))]
         [DynamicDependency("#ctor", typeof(IEnumerableOfTConverter<,>))]
         [DynamicDependency("#ctor", typeof(IEnumerableWithAddMethodConverter<>))]
         [DynamicDependency("#ctor", typeof(IListConverter<>))]
         [DynamicDependency("#ctor", typeof(IListOfTConverter<,>))]
-        [DynamicDependency("#ctor", typeof(ImmutableDictionaryOfStringTValueConverter<,>))]
+        [DynamicDependency("#ctor", typeof(ImmutableDictionaryOfTKeyTValueConverter<,,>))]
         [DynamicDependency("#ctor", typeof(ImmutableEnumerableOfTConverter<,>))]
-        [DynamicDependency("#ctor", typeof(IReadOnlyDictionaryOfStringTValueConverter<,>))]
+        [DynamicDependency("#ctor", typeof(IReadOnlyDictionaryOfTKeyTValueConverter<,,>))]
         [DynamicDependency("#ctor", typeof(ISetOfTConverter<,>))]
         [DynamicDependency("#ctor", typeof(ListOfTConverter<,>))]
         [DynamicDependency("#ctor", typeof(QueueOfTConverter<,>))]
         [DynamicDependency("#ctor", typeof(StackOfTConverter<,>))]
         public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
         {
-            Type converterType = null!;
+            Type converterType;
             Type[] genericArgs;
             Type? elementType = null;
+            Type? dictionaryKeyType = null;
             Type? actualTypeToConvert;
 
             // Array
@@ -67,61 +68,37 @@ namespace System.Text.Json.Serialization.Converters
                 converterType = typeof(ListOfTConverter<,>);
                 elementType = actualTypeToConvert.GetGenericArguments()[0];
             }
-            // Dictionary<string,> or deriving from Dictionary<string,>
+            // Dictionary<TKey, TValue> or deriving from Dictionary<TKey, TValue>
             else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericBaseClass(typeof(Dictionary<,>))) != null)
             {
                 genericArgs = actualTypeToConvert.GetGenericArguments();
-                if (genericArgs[0] == typeof(string))
-                {
-                    converterType = typeof(DictionaryOfStringTValueConverter<,>);
-                    elementType = genericArgs[1];
-                }
-                else
-                {
-                    ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(typeToConvert);
-                }
+                converterType = typeof(DictionaryOfTKeyTValueConverter<,,>);
+                dictionaryKeyType = genericArgs[0];
+                elementType = genericArgs[1];
             }
-            // Immutable dictionaries from System.Collections.Immutable, e.g. ImmutableDictionary<string, TValue>
+            // Immutable dictionaries from System.Collections.Immutable, e.g. ImmutableDictionary<TKey, TValue>
             else if (typeToConvert.IsImmutableDictionaryType())
             {
                 genericArgs = typeToConvert.GetGenericArguments();
-                if (genericArgs[0] == typeof(string))
-                {
-                    converterType = typeof(ImmutableDictionaryOfStringTValueConverter<,>);
-                    elementType = genericArgs[1];
-                }
-                else
-                {
-                    ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(typeToConvert);
-                }
+                converterType = typeof(ImmutableDictionaryOfTKeyTValueConverter<,,>);
+                dictionaryKeyType = genericArgs[0];
+                elementType = genericArgs[1];
             }
-            // IDictionary<string,> or deriving from IDictionary<string,>
+            // IDictionary<TKey, TValue> or deriving from IDictionary<TKey, TValue>
             else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IDictionary<,>))) != null)
             {
                 genericArgs = actualTypeToConvert.GetGenericArguments();
-                if (genericArgs[0] == typeof(string))
-                {
-                    converterType = typeof(IDictionaryOfStringTValueConverter<,>);
-                    elementType = genericArgs[1];
-                }
-                else
-                {
-                    ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(typeToConvert);
-                }
+                converterType = typeof(IDictionaryOfTKeyTValueConverter<,,>);
+                dictionaryKeyType = genericArgs[0];
+                elementType = genericArgs[1];
             }
-            // IReadOnlyDictionary<string,> or deriving from IReadOnlyDictionary<string,>
+            // IReadOnlyDictionary<TKey, TValue> or deriving from IReadOnlyDictionary<TKey, TValue>
             else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IReadOnlyDictionary<,>))) != null)
             {
                 genericArgs = actualTypeToConvert.GetGenericArguments();
-                if (genericArgs[0] == typeof(string))
-                {
-                    converterType = typeof(IReadOnlyDictionaryOfStringTValueConverter<,>);
-                    elementType = genericArgs[1];
-                }
-                else
-                {
-                    ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(typeToConvert);
-                }
+                converterType = typeof(IReadOnlyDictionaryOfTKeyTValueConverter<,,>);
+                dictionaryKeyType = genericArgs[0];
+                elementType = genericArgs[1];
             }
             // Immutable non-dictionaries from System.Collections.Immutable, e.g. ImmutableStack<T>
             else if (typeToConvert.IsImmutableEnumerableType())
@@ -211,17 +188,21 @@ namespace System.Text.Json.Serialization.Converters
                 converterType = typeof(IEnumerableConverter<>);
             }
 
-            Debug.Assert(converterType != null);
-
             Type genericType;
-            if (converterType.GetGenericArguments().Length == 1)
+            int numberOfGenericArgs = converterType.GetGenericArguments().Length;
+            if (numberOfGenericArgs == 1)
             {
                 genericType = converterType.MakeGenericType(typeToConvert);
             }
-            else
+            else if (numberOfGenericArgs == 2)
             {
                 genericType = converterType.MakeGenericType(typeToConvert, elementType!);
             }
+            else
+            {
+                Debug.Assert(numberOfGenericArgs == 3);
+                genericType = converterType.MakeGenericType(typeToConvert, dictionaryKeyType!, elementType!);
+            }
 
             JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                 genericType,
@@ -6,14 +6,14 @@ using System.Collections.Generic;
 
 namespace System.Text.Json.Serialization.Converters
 {
-    internal sealed class IReadOnlyDictionaryOfStringTValueConverter<TCollection, TValue>
-        : DictionaryDefaultConverter<TCollection, TValue>
-        where TCollection : IReadOnlyDictionary<string, TValue>
+    internal sealed class IReadOnlyDictionaryOfTKeyTValueConverter<TCollection, TKey, TValue>
+        : DictionaryDefaultConverter<TCollection, TKey, TValue>
+        where TCollection : IReadOnlyDictionary<TKey, TValue>
+        where TKey : notnull
     {
-        protected override void Add(in TValue value, JsonSerializerOptions options, ref ReadStack state)
+        protected override void Add(TKey key, in TValue value, JsonSerializerOptions options, ref ReadStack state)
         {
-            string key = state.Current.JsonPropertyNameAsString!;
-            ((Dictionary<string, TValue>)state.Current.ReturnValue!)[key] = value;
+            ((Dictionary<TKey, TValue>)state.Current.ReturnValue!)[key] = value;
         }
 
         protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state)
@@ -28,7 +28,7 @@ namespace System.Text.Json.Serialization.Converters
 
         protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state)
         {
-            IEnumerator<KeyValuePair<string, TValue>> enumerator;
+            IEnumerator<KeyValuePair<TKey, TValue>> enumerator;
             if (state.Current.CollectionEnumerator == null)
             {
                 enumerator = value.GetEnumerator();
@@ -39,10 +39,11 @@ namespace System.Text.Json.Serialization.Converters
             }
             else
             {
-                enumerator = (Dictionary<string, TValue>.Enumerator)state.Current.CollectionEnumerator;
+                enumerator = (Dictionary<TKey, TValue>.Enumerator)state.Current.CollectionEnumerator;
             }
 
-            JsonConverter<TValue> converter = GetValueConverter(ref state);
+            JsonConverter<TKey> keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options);
+            JsonConverter<TValue> valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo);
             do
             {
                 if (ShouldFlush(writer, ref state))
@@ -54,12 +55,13 @@ namespace System.Text.Json.Serialization.Converters
                 if (state.Current.PropertyState < StackFramePropertyState.Name)
                 {
                     state.Current.PropertyState = StackFramePropertyState.Name;
-                    string key = GetKeyName(enumerator.Current.Key, ref state, options);
-                    writer.WritePropertyName(key);
+
+                    TKey key = enumerator.Current.Key;
+                    keyConverter.WriteWithQuotes(writer, key, options, ref state);
                 }
 
                 TValue element = enumerator.Current.Value;
-                if (!converter.TryWrite(writer, element, options, ref state))
+                if (!valueConverter.TryWrite(writer, element, options, ref state))
                 {
                     state.Current.CollectionEnumerator = enumerator;
                     return false;
@@ -71,6 +73,6 @@ namespace System.Text.Json.Serialization.Converters
             return true;
         }
 
-        internal override Type RuntimeType => typeof(Dictionary<string, TValue>);
+        internal override Type RuntimeType => typeof(Dictionary<TKey, TValue>);
     }
 }
@@ -6,14 +6,14 @@ using System.Collections.Generic;
 
 namespace System.Text.Json.Serialization.Converters
 {
-    internal sealed class ImmutableDictionaryOfStringTValueConverter<TCollection, TValue>
-        : DictionaryDefaultConverter<TCollection, TValue>
-        where TCollection : IReadOnlyDictionary<string, TValue>
+    internal sealed class ImmutableDictionaryOfTKeyTValueConverter<TCollection, TKey, TValue>
+        : DictionaryDefaultConverter<TCollection, TKey, TValue>
+        where TCollection : IReadOnlyDictionary<TKey, TValue>
+        where TKey : notnull
     {
-        protected override void Add(in TValue value, JsonSerializerOptions options, ref ReadStack state)
+        protected override void Add(TKey key, in TValue value, JsonSerializerOptions options, ref ReadStack state)
         {
-            string key = state.Current.JsonPropertyNameAsString!;
-            ((Dictionary<string, TValue>)state.Current.ReturnValue!)[key] = value;
+            ((Dictionary<TKey, TValue>)state.Current.ReturnValue!)[key] = value;
         }
 
         internal override bool CanHaveIdMetadata => false;
@@ -39,7 +39,7 @@ namespace System.Text.Json.Serialization.Converters
 
         protected internal override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, ref WriteStack state)
         {
-            IEnumerator<KeyValuePair<string, TValue>> enumerator;
+            IEnumerator<KeyValuePair<TKey, TValue>> enumerator;
             if (state.Current.CollectionEnumerator == null)
             {
                 enumerator = value.GetEnumerator();
@@ -50,10 +50,11 @@ namespace System.Text.Json.Serialization.Converters
             }
             else
             {
-                enumerator = (IEnumerator<KeyValuePair<string, TValue>>)state.Current.CollectionEnumerator;
+                enumerator = (IEnumerator<KeyValuePair<TKey, TValue>>)state.Current.CollectionEnumerator;
             }
 
-            JsonConverter<TValue> converter = GetValueConverter(ref state);
+            JsonConverter<TKey> keyConverter = _keyConverter ??= GetKeyConverter(KeyType, options);
+            JsonConverter<TValue> valueConverter = _valueConverter ??= GetValueConverter(state.Current.JsonClassInfo);
             do
             {
                 if (ShouldFlush(writer, ref state))
@@ -65,12 +66,13 @@ namespace System.Text.Json.Serialization.Converters
                 if (state.Current.PropertyState < StackFramePropertyState.Name)
                 {
                     state.Current.PropertyState = StackFramePropertyState.Name;
-                    string key = GetKeyName(enumerator.Current.Key, ref state, options);
-                    writer.WritePropertyName(key);
+
+                    TKey key = enumerator.Current.Key;
+                    keyConverter.WriteWithQuotes(writer, key, options, ref state);
                 }
 
                 TValue element = enumerator.Current.Value;
-                if (!converter.TryWrite(writer, element, options, ref state))
+                if (!valueConverter.TryWrite(writer, element, options, ref state))
                 {
                     state.Current.CollectionEnumerator = enumerator;
                     return false;
index 04da6ed..32aba9a 100644 (file)
@@ -2,6 +2,8 @@
 // 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.Buffers.Text;
+
 namespace System.Text.Json.Serialization.Converters
 {
     internal sealed class BooleanConverter : JsonConverter<bool>
@@ -15,5 +17,22 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteBooleanValue(value);
         }
+
+        internal override bool ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            ReadOnlySpan<byte> propertyName = reader.GetSpan();
+            if (Utf8Parser.TryParse(propertyName, out bool value, out int bytesConsumed)
+                && propertyName.Length == bytesConsumed)
+            {
+                return value;
+            }
+
+            throw ThrowHelper.GetFormatException(DataType.Boolean);
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, bool value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index 38b8211..1280d2a 100644 (file)
@@ -15,5 +15,15 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteNumberValue(value);
         }
+
+        internal override byte ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetByteWithQuotes();
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, byte value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index 5b25989..41901e3 100644 (file)
@@ -28,5 +28,19 @@ namespace System.Text.Json.Serialization.Converters
 #endif
                 );
         }
+
+        internal override char ReadWithQuotes(ref Utf8JsonReader reader)
+            => Read(ref reader, default!, default!);
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, char value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(
+#if BUILDING_INBOX_LIBRARY
+                MemoryMarshal.CreateSpan(ref value, 1)
+#else
+                value.ToString()
+#endif
+                );
+        }
     }
 }
index e5dbbd5..00754b5 100644 (file)
@@ -15,5 +15,15 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteStringValue(value);
         }
+
+        internal override DateTime ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetDateTimeNoValidation();
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index 927911c..e6eada9 100644 (file)
@@ -15,5 +15,15 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteStringValue(value);
         }
+
+        internal override DateTimeOffset ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetDateTimeOffsetNoValidation();
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index 326df60..0f2350f 100644 (file)
@@ -15,5 +15,15 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteNumberValue(value);
         }
+
+        internal override decimal ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetDecimalWithQuotes();
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index 4a2acd6..5814f89 100644 (file)
@@ -15,5 +15,15 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteNumberValue(value);
         }
+
+        internal override double ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetDoubleWithQuotes();
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, double value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index 41f7302..c4b4dd7 100644 (file)
@@ -83,15 +83,7 @@ namespace System.Text.Json.Serialization.Converters
                     return default;
                 }
 
-                // Try parsing case sensitive first
-                string? enumString = reader.GetString();
-                if (!Enum.TryParse(enumString, out T value)
-                    && !Enum.TryParse(enumString, ignoreCase: true, out value))
-                {
-                    ThrowHelper.ThrowJsonException();
-                    return default;
-                }
-                return value;
+                return ReadWithQuotes(ref reader);
             }
 
             if (token != JsonTokenType.Number || !_converterOptions.HasFlag(EnumConverterOptions.AllowNumbers))
@@ -317,5 +309,91 @@ namespace System.Text.Json.Serialization.Converters
 
             return converted;
         }
+
+        internal override T ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            string? enumString = reader.GetString();
+
+            // Try parsing case sensitive first
+            if (!Enum.TryParse(enumString, out T value)
+                && !Enum.TryParse(enumString, ignoreCase: true, out value))
+            {
+                ThrowHelper.ThrowJsonException();
+            }
+
+            return value;
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, T value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            // An EnumConverter that invokes this method
+            // can only be created by JsonSerializerOptions.GetDictionaryKeyConverter
+            // hence no naming policy is expected.
+            Debug.Assert(_namingPolicy == null);
+
+            ulong key = ConvertToUInt64(value);
+
+            if (_nameCache.TryGetValue(key, out JsonEncodedText formatted))
+            {
+                writer.WritePropertyName(formatted);
+                return;
+            }
+
+            string original = value.ToString();
+            if (IsValidIdentifier(original))
+            {
+                // We are dealing with a combination of flag constants since
+                // all constant values were cached during warm-up.
+                JavaScriptEncoder? encoder = options.Encoder;
+
+                if (_nameCache.Count < NameCacheSizeSoftLimit)
+                {
+                    formatted = JsonEncodedText.Encode(original, encoder);
+
+                    writer.WritePropertyName(formatted);
+
+                    _nameCache.TryAdd(key, formatted);
+                }
+                else
+                {
+                    // We also do not create a JsonEncodedText instance here because passing the string
+                    // directly to the writer is cheaper than creating one and not caching it for reuse.
+                    writer.WritePropertyName(original);
+                }
+
+                return;
+            }
+
+            switch (s_enumTypeCode)
+            {
+                case TypeCode.Int32:
+                    writer.WritePropertyName(Unsafe.As<T, int>(ref value));
+                    break;
+                case TypeCode.UInt32:
+                    writer.WritePropertyName(Unsafe.As<T, uint>(ref value));
+                    break;
+                case TypeCode.UInt64:
+                    writer.WritePropertyName(Unsafe.As<T, ulong>(ref value));
+                    break;
+                case TypeCode.Int64:
+                    writer.WritePropertyName(Unsafe.As<T, long>(ref value));
+                    break;
+                case TypeCode.Int16:
+                    writer.WritePropertyName(Unsafe.As<T, short>(ref value));
+                    break;
+                case TypeCode.UInt16:
+                    writer.WritePropertyName(Unsafe.As<T, ushort>(ref value));
+                    break;
+                case TypeCode.Byte:
+                    writer.WritePropertyName(Unsafe.As<T, byte>(ref value));
+                    break;
+                case TypeCode.SByte:
+                    writer.WritePropertyName(Unsafe.As<T, sbyte>(ref value));
+                    break;
+                default:
+                    ThrowHelper.ThrowJsonException();
+                    break;
+            }
+        }
     }
 }
index 3c7c010..6328144 100644 (file)
@@ -15,5 +15,15 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteStringValue(value);
         }
+
+        internal override Guid ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetGuidNoValidation();
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index f68e234..8104a98 100644 (file)
@@ -2,6 +2,8 @@
 // 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.Converters
 {
     internal sealed class Int16Converter : JsonConverter<short>
@@ -15,5 +17,15 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteNumberValue(value);
         }
+
+        internal override short ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetInt16WithQuotes();
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, short value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index abd9089..f6f6712 100644 (file)
@@ -15,5 +15,15 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteNumberValue(value);
         }
+
+        internal override int ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetInt32WithQuotes();
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, int value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index 0ea30c2..a8817af 100644 (file)
@@ -15,5 +15,15 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteNumberValue(value);
         }
+
+        internal override long ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetInt64WithQuotes();
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, long value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index acbae39..fe680f6 100644 (file)
@@ -18,5 +18,25 @@ namespace System.Text.Json.Serialization.Converters
         {
             throw new InvalidOperationException();
         }
+
+        internal override object ReadWithQuotes(ref Utf8JsonReader reader)
+            => throw new NotSupportedException();
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            JsonConverter runtimeConverter = GetRuntimeConverter(value.GetType(), options);
+            runtimeConverter.WriteWithQuotesAsObject(writer, value, options, ref state);
+        }
+
+        private JsonConverter GetRuntimeConverter(Type runtimeType, JsonSerializerOptions options)
+        {
+            JsonConverter runtimeConverter = options.GetDictionaryKeyConverter(runtimeType);
+            if (runtimeConverter == this)
+            {
+                ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(runtimeType);
+            }
+
+            return runtimeConverter;
+        }
     }
 }
index bbc004a..f4a2339 100644 (file)
@@ -15,5 +15,15 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteNumberValue(value);
         }
+
+        internal override sbyte ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetSByteWithQuotes();
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, sbyte value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index c7c8b18..1653a65 100644 (file)
@@ -15,5 +15,15 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteNumberValue(value);
         }
+
+        internal override float ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetSingleWithQuotes();
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, float value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index bd420a3..d0d958a 100644 (file)
@@ -2,9 +2,11 @@
 // 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.Converters
 {
-    internal sealed class StringConverter : JsonConverter<string?>
+    internal sealed class StringConverter : JsonConverter<string>
     {
         public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
         {
@@ -15,5 +17,25 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteStringValue(value);
         }
+
+        internal override string ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetString()!;
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, string value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            if (options.DictionaryKeyPolicy != null && !state.Current.IgnoreDictionaryKeyPolicy)
+            {
+                value = options.DictionaryKeyPolicy.ConvertName(value);
+
+                if (value == null)
+                {
+                    ThrowHelper.ThrowInvalidOperationException_NamingPolicyReturnNull(options.DictionaryKeyPolicy);
+                }
+            }
+
+            writer.WritePropertyName(value);
+        }
     }
 }
index 56f24b0..44f20a2 100644 (file)
@@ -15,5 +15,15 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteNumberValue(value);
         }
+
+        internal override ushort ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetUInt16WithQuotes();
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, ushort value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index 9dfc006..cea8390 100644 (file)
@@ -15,5 +15,15 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteNumberValue(value);
         }
+
+        internal override uint ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetUInt32WithQuotes();
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, uint value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index af26a45..3f07e36 100644 (file)
@@ -15,5 +15,15 @@ namespace System.Text.Json.Serialization.Converters
         {
             writer.WriteNumberValue(value);
         }
+
+        internal override ulong ReadWithQuotes(ref Utf8JsonReader reader)
+        {
+            return reader.GetUInt64WithQuotes();
+        }
+
+        internal override void WriteWithQuotes(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            writer.WritePropertyName(value);
+        }
     }
 }
index cc73208..25b43c8 100644 (file)
@@ -71,6 +71,11 @@ namespace System.Text.Json.Serialization
         /// </summary>
         internal abstract bool WriteCoreAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state);
 
+        /// <summary>
+        /// Loosely-typed WriteWithQuotes() that forwards to strongly-typed WriteWithQuotes().
+        /// </summary>
+        internal abstract void WriteWithQuotesAsObject(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state);
+
         // Whether a type (ClassType.Object) is deserialized using a parameterized constructor.
         internal virtual bool ConstructorIsParameterized => false;
 
index 5186002..0250731 100644 (file)
@@ -111,5 +111,15 @@ namespace System.Text.Json.Serialization
 
             throw new InvalidOperationException();
         }
+
+        internal sealed override void WriteWithQuotesAsObject(
+            Utf8JsonWriter writer, object value,
+            JsonSerializerOptions options,
+            ref WriteStack state)
+        {
+            Debug.Fail("We should never get here.");
+
+            throw new InvalidOperationException();
+        }
     }
 }
index 5689440..bfdcaaf 100644 (file)
@@ -335,8 +335,6 @@ namespace System.Text.Json.Serialization
 
             Debug.Assert(this is JsonDictionaryConverter<T>);
 
-            state.Current.PolymorphicJsonPropertyInfo = state.Current.DeclaredJsonPropertyInfo!.RuntimeClassInfo.ElementClassInfo!.PropertyInfoForClassInfo;
-
             if (writer.CurrentDepth >= options.EffectiveMaxDepth)
             {
                 ThrowHelper.ThrowJsonException_SerializerCycleDetected(options.EffectiveMaxDepth);
@@ -433,5 +431,14 @@ namespace System.Text.Json.Serialization
         /// <param name="value">The value to convert.</param>
         /// <param name="options">The <see cref="JsonSerializerOptions"/> being used.</param>
         public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options);
+
+        internal virtual T ReadWithQuotes(ref Utf8JsonReader reader)
+            => throw new InvalidOperationException();
+
+        internal virtual void WriteWithQuotes(Utf8JsonWriter writer, [DisallowNull] T value, JsonSerializerOptions options, ref WriteStack state)
+            => throw new InvalidOperationException();
+
+        internal sealed override void WriteWithQuotesAsObject(Utf8JsonWriter writer, object value, JsonSerializerOptions options, ref WriteStack state)
+            => WriteWithQuotes(writer, (T)value, options, ref state);
     }
 }
index bc1f798..fc63f11 100644 (file)
@@ -70,7 +70,72 @@ namespace System.Text.Json
             return converters;
 
             void Add(JsonConverter converter) =>
-                converters.Add(converter.TypeToConvert!, converter);
+                converters.Add(converter.TypeToConvert, converter);
+        }
+
+        internal JsonConverter GetDictionaryKeyConverter(Type keyType)
+        {
+            _dictionaryKeyConverters ??= GetDictionaryKeyConverters();
+
+            if (!_dictionaryKeyConverters.TryGetValue(keyType, out JsonConverter? converter))
+            {
+                if (keyType.IsEnum)
+                {
+                    converter = GetEnumConverter();
+                    _dictionaryKeyConverters[keyType] = converter;
+                }
+                else
+                {
+                    ThrowHelper.ThrowNotSupportedException_DictionaryKeyTypeNotSupported(keyType);
+                }
+            }
+
+            return converter!;
+
+            // Use factory pattern to generate an EnumConverter with AllowStrings and AllowNumbers options for dictionary keys.
+            // There will be one converter created for each enum type.
+            JsonConverter GetEnumConverter()
+                => (JsonConverter)Activator.CreateInstance(
+                        typeof(EnumConverter<>).MakeGenericType(keyType),
+                        BindingFlags.Instance | BindingFlags.Public,
+                        binder: null,
+                        new object[] { EnumConverterOptions.AllowStrings | EnumConverterOptions.AllowNumbers, this },
+                        culture: null)!;
+        }
+
+        private ConcurrentDictionary<Type, JsonConverter>? _dictionaryKeyConverters;
+
+        private static ConcurrentDictionary<Type, JsonConverter> GetDictionaryKeyConverters()
+        {
+            const int NumberOfConverters = 18;
+            var converters = new ConcurrentDictionary<Type, JsonConverter>(Environment.ProcessorCount, NumberOfConverters);
+
+            // When adding to this, update NumberOfConverters above.
+            Add(s_defaultSimpleConverters[typeof(bool)]);
+            Add(s_defaultSimpleConverters[typeof(byte)]);
+            Add(s_defaultSimpleConverters[typeof(char)]);
+            Add(s_defaultSimpleConverters[typeof(DateTime)]);
+            Add(s_defaultSimpleConverters[typeof(DateTimeOffset)]);
+            Add(s_defaultSimpleConverters[typeof(double)]);
+            Add(s_defaultSimpleConverters[typeof(decimal)]);
+            Add(s_defaultSimpleConverters[typeof(Guid)]);
+            Add(s_defaultSimpleConverters[typeof(short)]);
+            Add(s_defaultSimpleConverters[typeof(int)]);
+            Add(s_defaultSimpleConverters[typeof(long)]);
+            Add(s_defaultSimpleConverters[typeof(object)]);
+            Add(s_defaultSimpleConverters[typeof(sbyte)]);
+            Add(s_defaultSimpleConverters[typeof(float)]);
+            Add(s_defaultSimpleConverters[typeof(string)]);
+            Add(s_defaultSimpleConverters[typeof(ushort)]);
+            Add(s_defaultSimpleConverters[typeof(uint)]);
+            Add(s_defaultSimpleConverters[typeof(ulong)]);
+
+            Debug.Assert(NumberOfConverters == converters.Count);
+
+            return converters;
+
+            void Add(JsonConverter converter) =>
+                converters[converter.TypeToConvert] = converter;
         }
 
         /// <summary>
index d9a5947..74ff1bc 100644 (file)
@@ -15,9 +15,14 @@ namespace System.Text.Json
         public StackFramePropertyState PropertyState;
         public bool UseExtensionProperty;
 
-        // Support JSON Path on exceptions.
-        public byte[]? JsonPropertyName; // This is Utf8 since we don't want to convert to string until an exception is thown.
-        public string? JsonPropertyNameAsString; // This is used for dictionary keys and re-entry cases that specify a property name.
+        // Support JSON Path on exceptions and non-string Dictionary keys.
+        // This is Utf8 since we don't want to convert to string until an exception is thown.
+        // For dictionary keys we don't want to convert to TKey until we have both key and value when parsing the dictionary elements on stream cases.
+        public byte[]? JsonPropertyName;
+        public string? JsonPropertyNameAsString; // This is used for string dictionary keys and re-entry cases that specify a property name.
+
+        // Stores the non-string dictionary keys for continuation.
+        public object? DictionaryKey;
 
         // Validation state.
         public int OriginalDepth;
index 71d3f2a..a27c198 100644 (file)
@@ -42,6 +42,13 @@ namespace System.Text.Json
 
         [DoesNotReturn]
         [MethodImpl(MethodImplOptions.NoInlining)]
+        public static void ThrowNotSupportedException_DictionaryKeyTypeNotSupported(Type keyType)
+        {
+            throw new NotSupportedException(SR.Format(SR.DictionaryKeyTypeNotSupported, keyType));
+        }
+
+        [DoesNotReturn]
+        [MethodImpl(MethodImplOptions.NoInlining)]
         public static void ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType)
         {
             var ex = new JsonException(SR.Format(SR.DeserializeUnableToConvertValue, propertyType));
index 9dffc28..16b2471 100644 (file)
@@ -612,6 +612,9 @@ namespace System.Text.Json
 
             switch (dateType)
             {
+                case DataType.Boolean:
+                    message = SR.FormatBoolean;
+                    break;
                 case DataType.DateTime:
                     message = SR.FormatDateTime;
                     break;
@@ -702,6 +705,7 @@ namespace System.Text.Json
 
     internal enum DataType
     {
+        Boolean,
         DateTime,
         DateTimeOffset,
         Base64String,
index da5219b..0ed995b 100644 (file)
@@ -375,5 +375,12 @@ namespace System.Text.Json
 
             output[BytesPending++] = JsonConstants.Quote;
         }
+
+        internal void WritePropertyName(DateTime value)
+        {
+            Span<byte> buffer = stackalloc byte[JsonConstants.MaximumFormatDateTimeOffsetLength];
+            JsonWriterHelper.WriteDateTimeTrimmed(buffer, value, out int bytesWritten);
+            WritePropertyNameUnescaped(buffer.Slice(0, bytesWritten));
+        }
     }
 }
index 37cd479..62ba78f 100644 (file)
@@ -374,5 +374,12 @@ namespace System.Text.Json
 
             output[BytesPending++] = JsonConstants.Quote;
         }
+
+        internal void WritePropertyName(DateTimeOffset value)
+        {
+            Span<byte> buffer = stackalloc byte[JsonConstants.MaximumFormatDateTimeOffsetLength];
+            JsonWriterHelper.WriteDateTimeOffsetTrimmed(buffer, value, out int bytesWritten);
+            WritePropertyNameUnescaped(buffer.Slice(0, bytesWritten));
+        }
     }
 }
index e4b05e7..6260987 100644 (file)
@@ -362,5 +362,13 @@ namespace System.Text.Json
             Debug.Assert(result);
             BytesPending += bytesWritten;
         }
+
+        internal void WritePropertyName(decimal value)
+        {
+            Span<byte> utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatDecimalLength];
+            bool result = Utf8Formatter.TryFormat(value, utf8PropertyName, out int bytesWritten);
+            Debug.Assert(result);
+            WritePropertyNameUnescaped(utf8PropertyName.Slice(0, bytesWritten));
+        }
     }
 }
index f8ae05c..6304753 100644 (file)
@@ -366,5 +366,14 @@ namespace System.Text.Json
             Debug.Assert(result);
             BytesPending += bytesWritten;
         }
+
+        internal void WritePropertyName(double value)
+        {
+            JsonWriterHelper.ValidateDouble(value);
+            Span<byte> utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatDoubleLength];
+            bool result = TryFormatDouble(value, utf8PropertyName, out int bytesWritten);
+            Debug.Assert(result);
+            WritePropertyNameUnescaped(utf8PropertyName.Slice(0, bytesWritten));
+        }
     }
 }
index 60ba0b0..2af8936 100644 (file)
@@ -366,5 +366,13 @@ namespace System.Text.Json
             Debug.Assert(result);
             BytesPending += bytesWritten;
         }
+
+        internal void WritePropertyName(float value)
+        {
+            Span<byte> utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatSingleLength];
+            bool result = TryFormatSingle(value, utf8PropertyName, out int bytesWritten);
+            Debug.Assert(result);
+            WritePropertyNameUnescaped(utf8PropertyName.Slice(0, bytesWritten));
+        }
     }
 }
index b18a107..062e1ea 100644 (file)
@@ -378,5 +378,13 @@ namespace System.Text.Json
 
             output[BytesPending++] = JsonConstants.Quote;
         }
+
+        internal void WritePropertyName(Guid value)
+        {
+            Span<byte> utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatGuidLength];
+            bool result = Utf8Formatter.TryFormat(value, utf8PropertyName, out int bytesWritten);
+            Debug.Assert(result);
+            WritePropertyNameUnescaped(utf8PropertyName.Slice(0, bytesWritten));
+        }
     }
 }
index c5772f9..625d771 100644 (file)
@@ -3,6 +3,7 @@
 // See the LICENSE file in the project root for more information.
 
 using System.Buffers;
+using System.Buffers.Text;
 using System.Diagnostics;
 using System.Runtime.CompilerServices;
 
@@ -503,5 +504,15 @@ namespace System.Text.Json
             value.CopyTo(output.Slice(BytesPending));
             BytesPending += value.Length;
         }
+
+        internal void WritePropertyName(bool value)
+        {
+            Span<byte> utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatBooleanLength];
+
+            bool result = Utf8Formatter.TryFormat(value, utf8PropertyName, out int bytesWritten);
+            Debug.Assert(result);
+
+            WritePropertyNameUnescaped(utf8PropertyName.Slice(0, bytesWritten));
+        }
     }
 }
index 360bea1..f24aad6 100644 (file)
@@ -432,5 +432,18 @@ namespace System.Text.Json
             Debug.Assert(result);
             BytesPending += bytesWritten;
         }
+
+        internal void WritePropertyName(int value)
+            => WritePropertyName((long)value);
+
+        internal void WritePropertyName(long value)
+        {
+            Span<byte> utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatInt64Length];
+
+            bool result = Utf8Formatter.TryFormat(value, utf8PropertyName, out int bytesWritten);
+            Debug.Assert(result);
+
+            WritePropertyNameUnescaped(utf8PropertyName.Slice(0, bytesWritten));
+        }
     }
 }
index ddcca39..d0c19f6 100644 (file)
@@ -255,6 +255,15 @@ namespace System.Text.Json
             _tokenType = JsonTokenType.PropertyName;
         }
 
+        private void WritePropertyNameUnescaped(ReadOnlySpan<byte> utf8PropertyName)
+        {
+            JsonWriterHelper.ValidateProperty(utf8PropertyName);
+            WriteStringByOptionsPropertyName(utf8PropertyName);
+
+            _currentDepth &= JsonConstants.RemoveFlagsBitMask;
+            _tokenType = JsonTokenType.PropertyName;
+        }
+
         private void WriteStringEscapeProperty(ReadOnlySpan<byte> utf8PropertyName, int firstEscapeIndexProp)
         {
             Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length);
index 48d9518..0430768 100644 (file)
@@ -441,5 +441,18 @@ namespace System.Text.Json
             Debug.Assert(result);
             BytesPending += bytesWritten;
         }
+
+        internal void WritePropertyName(uint value)
+            => WritePropertyName((ulong)value);
+
+        internal void WritePropertyName(ulong value)
+        {
+            Span<byte> utf8PropertyName = stackalloc byte[JsonConstants.MaximumFormatUInt64Length];
+
+            bool result = Utf8Formatter.TryFormat(value, utf8PropertyName, out int bytesWritten);
+            Debug.Assert(result);
+
+            WritePropertyNameUnescaped(utf8PropertyName.Slice(0, bytesWritten));
+        }
     }
 }
diff --git a/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Dictionary.NonStringKey.cs b/src/libraries/System.Text.Json/tests/Serialization/CollectionTests/CollectionTests.Dictionary.NonStringKey.cs
new file mode 100644 (file)
index 0000000..98b96d5
--- /dev/null
@@ -0,0 +1,579 @@
+// 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.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+    public partial class DictionaryTests
+    {
+        public abstract class DictionaryKeyTestsBase<TKey, TValue>
+        {
+            protected abstract TKey Key { get; }
+            protected abstract TValue Value { get; }
+            protected virtual string _expectedJson => $"{{\"{Key}\":{Value}}}";
+
+            protected virtual void Validate(Dictionary<TKey, TValue> dictionary)
+            {
+                bool success = dictionary.TryGetValue(Key, out TValue value);
+                Assert.True(success);
+                Assert.Equal(Value, value);
+            }
+
+            private Dictionary<TKey, TValue> BuildDictionary()
+            {
+                var dictionary = new Dictionary<TKey, TValue>();
+                dictionary.Add(Key, Value);
+
+                return dictionary;
+            }
+
+            [Fact]
+            public void TestDictionaryKey()
+            {
+                Dictionary<TKey, TValue> dictionary = BuildDictionary();
+
+                string json = JsonSerializer.Serialize(dictionary);
+                Assert.Equal(_expectedJson, json);
+
+                Dictionary<TKey, TValue> dictionaryCopy = JsonSerializer.Deserialize<Dictionary<TKey, TValue>>(json);
+                Validate(dictionaryCopy);
+            }
+
+            [Fact]
+            public async Task TestDictionaryKeyAsync()
+            {
+                Dictionary<TKey, TValue> dictionary = BuildDictionary();
+
+                MemoryStream serializeStream = new MemoryStream();
+                await JsonSerializer.SerializeAsync(serializeStream, dictionary);
+                string json = Encoding.UTF8.GetString(serializeStream.ToArray());
+                Assert.Equal(_expectedJson, json);
+
+                byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
+                Stream deserializeStream = new MemoryStream(jsonBytes);
+                Dictionary<TKey, TValue> dictionaryCopy = await JsonSerializer.DeserializeAsync<Dictionary<TKey, TValue>>(deserializeStream);
+                Validate(dictionaryCopy);
+            }
+        }
+
+        public class DictionaryBoolKey : DictionaryKeyTestsBase<bool, int>
+        {
+            protected override bool Key => true;
+            protected override int Value => 1;
+        }
+
+        public class DictionaryByteKey : DictionaryKeyTestsBase<byte, int>
+        {
+            protected override byte Key => byte.MaxValue;
+            protected override int Value => 1;
+        }
+
+        public class DictionaryCharKey : DictionaryKeyTestsBase<char, char>
+        {
+            protected override string _expectedJson => @"{""\uFFFF"":""\uFFFF""}";
+            protected override char Key => char.MaxValue;
+            protected override char Value => char.MaxValue;
+        }
+
+        public class DictionaryDateTimeKey : DictionaryKeyTestsBase<DateTime, int>
+        {
+            protected override string _expectedJson => $@"{{""{DateTime.MaxValue:O}"":1}}";
+            protected override DateTime Key => DateTime.MaxValue;
+            protected override int Value => 1;
+        }
+
+        public class DictionaryDateTimeOffsetKey : DictionaryKeyTestsBase<DateTimeOffset, int>
+        {
+            protected override string _expectedJson => $@"{{""{DateTimeOffset.MaxValue:O}"":1}}";
+            protected override DateTimeOffset Key => DateTimeOffset.MaxValue;
+            protected override int Value => 1;
+        }
+
+        public class DictionaryDecimalKey : DictionaryKeyTestsBase<decimal, int>
+        {
+            protected override string _expectedJson => $@"{{""{JsonSerializer.Serialize(decimal.MaxValue)}"":1}}";
+            protected override decimal Key => decimal.MaxValue;
+            protected override int Value => 1;
+        }
+
+        public class DictionaryDoubleKey : DictionaryKeyTestsBase<double, int>
+        {
+            protected override string _expectedJson => $@"{{""{JsonSerializer.Serialize(double.MaxValue)}"":1}}";
+            protected override double Key => double.MaxValue;
+            protected override int Value => 1;
+        }
+
+        public class DictionaryEnumKey : DictionaryKeyTestsBase<MyEnum, int>
+        {
+            protected override MyEnum Key => MyEnum.Foo;
+            protected override int Value => 1;
+        }
+
+        public class DictionaryEnumFlagsKey : DictionaryKeyTestsBase<MyEnumFlags, int>
+        {
+            protected override MyEnumFlags Key => MyEnumFlags.Foo | MyEnumFlags.Bar;
+            protected override int Value => 1;
+        }
+
+        public class DictionaryGuidKey : DictionaryKeyTestsBase<Guid, int>
+        {
+            // Use singleton pattern here so the Guid key does not change everytime this is called.
+            protected override Guid Key { get; } = Guid.NewGuid();
+            protected override int Value => 1;
+        }
+
+        public class DictionaryInt16Key : DictionaryKeyTestsBase<short, int>
+        {
+            protected override short Key => short.MaxValue;
+            protected override int Value => 1;
+        }
+
+        public class DictionaryInt32Key : DictionaryKeyTestsBase<int, int>
+        {
+            protected override int Key => int.MaxValue;
+            protected override int Value => 1;
+        }
+
+        public class DictionaryInt64Key : DictionaryKeyTestsBase<long, int>
+        {
+            protected override long Key => long.MaxValue;
+            protected override int Value => 1;
+        }
+
+        public class DictionarySByteKey : DictionaryKeyTestsBase<sbyte, int>
+        {
+            protected override sbyte Key => sbyte.MaxValue;
+            protected override int Value => 1;
+        }
+
+        public class DictionarySingleKey : DictionaryKeyTestsBase<float, int>
+        {
+            protected override string _expectedJson => $@"{{""{JsonSerializer.Serialize(float.MaxValue)}"":1}}";
+            protected override float Key => float.MaxValue;
+            protected override int Value => 1;
+        }
+
+        public class DictionaryStringKey : DictionaryKeyTestsBase<string, int>
+        {
+            protected override string Key => "KeyString";
+            protected override int Value => 1;
+        }
+
+        public class DictionaryUInt16Key : DictionaryKeyTestsBase<ushort, int>
+        {
+            protected override ushort Key => ushort.MaxValue;
+            protected override int Value => 1;
+        }
+
+        public class DictionaryUInt32Key : DictionaryKeyTestsBase<uint, int>
+        {
+            protected override uint Key => uint.MaxValue;
+            protected override int Value => 1;
+        }
+
+        public class DictionaryUInt64Key : DictionaryKeyTestsBase<ulong, int>
+        {
+            protected override ulong Key => ulong.MaxValue;
+            protected override int Value => 1;
+        }
+
+        public abstract class DictionaryUnsupportedKeyTestsBase<TKey, TValue>
+        {
+            private Dictionary<TKey, TValue> _dictionary => BuildDictionary();
+            protected abstract TKey Key { get; }
+            private Dictionary<TKey, TValue> BuildDictionary()
+            {
+                return new Dictionary<TKey, TValue>() { { Key, default } };
+            }
+
+            [Fact]
+            public void ThrowUnsupported_Serialize()
+                => Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(_dictionary));
+
+            [Fact]
+            public Task ThrowUnsupported_SerializeAsync()
+                => Assert.ThrowsAsync<NotSupportedException>(() => JsonSerializer.SerializeAsync(new MemoryStream(), _dictionary));
+
+            [Fact]
+            public void ThrowUnsupported_Deserialize() => Assert.Throws<NotSupportedException>(()
+                => JsonSerializer.Deserialize<Dictionary<TKey, TValue>>(@"{""foo"":1}"));
+
+            [Fact]
+            public Task ThrowUnsupported_DeserializeAsync() => Assert.ThrowsAsync<NotSupportedException>(()
+                => JsonSerializer.DeserializeAsync<Dictionary<TKey, TValue>>(new MemoryStream(Encoding.UTF8.GetBytes(@"{""foo"":1}"))).AsTask());
+
+            [Fact]
+            public void DoesNotThrowIfEmpty_Serialize()
+                => JsonSerializer.Serialize(new Dictionary<TKey, TValue>());
+
+            [Fact]
+            public Task DoesNotThrowIfEmpty_SerializeAsync()
+                => JsonSerializer.SerializeAsync(new MemoryStream(), new Dictionary<TKey, TValue>());
+
+            [Fact]
+            public void DoesNotThrowIfEmpty_Deserialize()
+                => JsonSerializer.Deserialize<Dictionary<TKey, TValue>>("{}");
+
+            [Fact]
+            public Task DoesNotThrowIfEmpty_DeserializeAsync()
+                => JsonSerializer.DeserializeAsync<Dictionary<TKey, TValue>>(new MemoryStream(Encoding.UTF8.GetBytes("{}"))).AsTask();
+        }
+
+        public class DictionaryMyPublicClassKeyUnsupported : DictionaryUnsupportedKeyTestsBase<MyPublicClass, int>
+        {
+            protected override MyPublicClass Key => new MyPublicClass();
+        }
+
+        public class DictionaryMyPublicStructKeyUnsupported : DictionaryUnsupportedKeyTestsBase<MyPublicStruct, int>
+        {
+            protected override MyPublicStruct Key => new MyPublicStruct();
+        }
+
+        public class DictionaryUriKeyUnsupported : DictionaryUnsupportedKeyTestsBase<Uri, int>
+        {
+            protected override Uri Key => new Uri("http://foo");
+        }
+
+        public class DictionaryObjectKeyUnsupported : DictionaryUnsupportedKeyTestsBase<object, int>
+        {
+            protected override object Key => new object();
+        }
+
+        public class DictionaryPolymorphicKeyUnsupported : DictionaryUnsupportedKeyTestsBase<object, int>
+        {
+            protected override object Key => new Uri("http://foo");
+        }
+
+        public class DictionaryNonStringKeyTests
+        {
+            [Fact]
+            public void TestGenericDictionaryKeyObject()
+            {
+                var dictionary = new Dictionary<object, object>();
+                // Add multiple supported types.
+                dictionary.Add(1, 1);
+                dictionary.Add(new Guid("08314FA2-B1FE-4792-BCD1-6E62338AC7F3"), 2);
+                dictionary.Add("KeyString", 3);
+                dictionary.Add(MyEnum.Foo, 4);
+                dictionary.Add(MyEnumFlags.Foo | MyEnumFlags.Bar, 5);
+
+                const string expected = @"{""1"":1,""08314fa2-b1fe-4792-bcd1-6e62338ac7f3"":2,""KeyString"":3,""Foo"":4,""Foo, Bar"":5}";
+
+                string json = JsonSerializer.Serialize(dictionary);
+                Assert.Equal(expected, json);
+                // object type is not supported on deserialization.
+                Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<Dictionary<object, object>>(json));
+
+                var @object = new ClassWithDictionary { Dictionary = dictionary };
+                json = JsonSerializer.Serialize(@object);
+                Assert.Equal($@"{{""Dictionary"":{expected}}}", json);
+                Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<ClassWithDictionary>(json));
+            }
+
+            [Fact]
+            public void TestNonGenericDictionaryKeyObject()
+            {
+                IDictionary dictionary = new OrderedDictionary();
+                // Add multiple supported types.
+                dictionary.Add(1, 1);
+                dictionary.Add(new Guid("08314FA2-B1FE-4792-BCD1-6E62338AC7F3"), 2);
+                dictionary.Add("KeyString", 3);
+                dictionary.Add(MyEnum.Foo, 4);
+                dictionary.Add(MyEnumFlags.Foo | MyEnumFlags.Bar, 5);
+
+                const string expected = @"{""1"":1,""08314fa2-b1fe-4792-bcd1-6e62338ac7f3"":2,""KeyString"":3,""Foo"":4,""Foo, Bar"":5}";
+                string json = JsonSerializer.Serialize(dictionary);
+                Assert.Equal(expected, json);
+
+                dictionary = JsonSerializer.Deserialize<IDictionary>(json);
+                Assert.IsType<Dictionary<string, object>>(dictionary);
+
+                dictionary = JsonSerializer.Deserialize<OrderedDictionary>(json);
+                foreach (object key in dictionary.Keys)
+                {
+                    Assert.IsType<string>(key);
+                }
+
+                var @object = new ClassWithIDictionary { Dictionary = dictionary };
+                json = JsonSerializer.Serialize(@object);
+                Assert.Equal($@"{{""Dictionary"":{expected}}}", json);
+
+                @object = JsonSerializer.Deserialize<ClassWithIDictionary>(json);
+                Assert.IsType<Dictionary<string, object>>(@object.Dictionary);
+            }
+
+            [Theory] // Extend this test when support for more types is added.
+            [InlineData(@"{""1.1"":1}", typeof(Dictionary<int, int>))]
+            [InlineData(@"{""{00000000-0000-0000-0000-000000000000}"":1}", typeof(Dictionary<Guid, int>))]
+            public void ThrowOnInvalidFormat(string json, Type typeToConvert)
+            {
+                JsonException ex = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize(json, typeToConvert));
+                Assert.Contains(typeToConvert.ToString(), ex.Message);
+            }
+
+            [Theory] // Extend this test when support for more types is added.
+            [InlineData(@"{""1.1"":1}", typeof(Dictionary<int, int>))]
+            [InlineData(@"{""{00000000-0000-0000-0000-000000000000}"":1}", typeof(Dictionary<Guid, int>))]
+            public async Task ThrowOnInvalidFormatAsync(string json, Type typeToConvert)
+            {
+                byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
+                Stream stream = new MemoryStream(jsonBytes);
+
+                JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await JsonSerializer.DeserializeAsync(stream, typeToConvert));
+                Assert.Contains(typeToConvert.ToString(), ex.Message);
+            }
+
+            [Fact]
+            public static void TestNotSuportedExceptionIsThrown()
+            {
+                // Dictionary<int[], int>>
+                Assert.Null(JsonSerializer.Deserialize<Dictionary<int[], int>>("null"));
+                Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<Dictionary<int[], int>>("\"\""));
+                Assert.NotNull(JsonSerializer.Deserialize<Dictionary<int[], int>>("{}"));
+
+                Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<Dictionary<int[], int>>(@"{""Foo"":1}"));
+
+                // UnsupportedDictionaryWrapper
+                Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<UnsupportedDictionaryWrapper>("\"\""));
+                Assert.NotNull(JsonSerializer.Deserialize<UnsupportedDictionaryWrapper>("{}"));
+                Assert.Null(JsonSerializer.Deserialize<UnsupportedDictionaryWrapper>("null"));
+                Assert.NotNull(JsonSerializer.Deserialize<UnsupportedDictionaryWrapper>(@"{""Dictionary"":null}"));
+                Assert.NotNull(JsonSerializer.Deserialize<UnsupportedDictionaryWrapper>(@"{""Dictionary"":{}}"));
+
+                Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<UnsupportedDictionaryWrapper>(@"{""Dictionary"":{""Foo"":1}}"));
+            }
+
+            [Fact]
+            public void TestPolicyOnlyAppliesToString()
+            {
+                var opts = new JsonSerializerOptions
+                {
+                    DictionaryKeyPolicy = new FixedNamingPolicy()
+                };
+
+                var stringIntDictionary = new Dictionary<string, int> { { "1", 1 } };
+                string json = JsonSerializer.Serialize(stringIntDictionary, opts);
+                Assert.Equal($@"{{""{FixedNamingPolicy.FixedName}"":1}}", json);
+
+                var intIntDictionary = new Dictionary<int, int> { { 1, 1 } };
+                json = JsonSerializer.Serialize(intIntDictionary, opts);
+                Assert.Equal(@"{""1"":1}", json);
+
+                var objectIntDictionary = new Dictionary<object, int> { { "1", 1 } };
+                json = JsonSerializer.Serialize(objectIntDictionary, opts);
+                Assert.Equal($@"{{""{FixedNamingPolicy.FixedName}"":1}}", json);
+
+                objectIntDictionary = new Dictionary<object, int> { { 1, 1 } };
+                json = JsonSerializer.Serialize(objectIntDictionary, opts);
+                Assert.Equal(@"{""1"":1}", json);
+            }
+
+            [Fact]
+            public async Task TestPolicyOnlyAppliesToStringAsync()
+            {
+                var opts = new JsonSerializerOptions
+                {
+                    DictionaryKeyPolicy = new FixedNamingPolicy()
+                };
+
+                MemoryStream stream = new MemoryStream();
+
+                var stringIntDictionary = new Dictionary<string, int> { { "1", 1 } };
+                await JsonSerializer.SerializeAsync(stream, stringIntDictionary, opts);
+
+                string json = Encoding.UTF8.GetString(stream.ToArray());
+                Assert.Equal($@"{{""{FixedNamingPolicy.FixedName}"":1}}", json);
+
+                stream.Position = 0;
+                stream.SetLength(0);
+
+                var intIntDictionary = new Dictionary<int, int> { { 1, 1 } };
+                await JsonSerializer.SerializeAsync(stream, intIntDictionary, opts);
+
+                json = Encoding.UTF8.GetString(stream.ToArray());
+                Assert.Equal(@"{""1"":1}", json);
+
+                stream.Position = 0;
+                stream.SetLength(0);
+
+                var objectIntDictionary = new Dictionary<object, int> { { "1", 1 } };
+                await JsonSerializer.SerializeAsync(stream, objectIntDictionary, opts);
+
+                json = Encoding.UTF8.GetString(stream.ToArray());
+                Assert.Equal($@"{{""{FixedNamingPolicy.FixedName}"":1}}", json);
+
+                stream.Position = 0;
+                stream.SetLength(0);
+
+                objectIntDictionary = new Dictionary<object, int> { { 1, 1 } };
+                await JsonSerializer.SerializeAsync(stream, objectIntDictionary, opts);
+
+                json = Encoding.UTF8.GetString(stream.ToArray());
+                Assert.Equal(@"{""1"":1}", json);
+            }
+
+            [Fact]
+            public void TestEnumKeyWithNotValidIdentifier()
+            {
+                var myEnumIntDictionary = new Dictionary<MyEnum, int>();
+                myEnumIntDictionary.Add((MyEnum)(-1), 1);
+
+                string json = JsonSerializer.Serialize(myEnumIntDictionary);
+                Assert.Equal(@"{""-1"":1}", json);
+
+                myEnumIntDictionary = JsonSerializer.Deserialize<Dictionary<MyEnum, int>>(json);
+                Assert.Equal(1, myEnumIntDictionary[(MyEnum)(-1)]);
+
+                var myEnumFlagsIntDictionary = new Dictionary<MyEnumFlags, int>();
+                myEnumFlagsIntDictionary.Add((MyEnumFlags)(-1), 1);
+
+                json = JsonSerializer.Serialize(myEnumFlagsIntDictionary);
+                Assert.Equal(@"{""-1"":1}", json);
+
+                myEnumFlagsIntDictionary = JsonSerializer.Deserialize<Dictionary<MyEnumFlags, int>>(json);
+                Assert.Equal(1, myEnumFlagsIntDictionary[(MyEnumFlags)(-1)]);
+            }
+
+            [Theory]
+            [MemberData(nameof(DictionaryKeysWithSpecialCharacters))]
+            public void EnsureNonStringKeysDontGetEscapedOnSerialize(object key, string expectedKeySerialized)
+            {
+                Dictionary<object, int> root = new Dictionary<object, int>();
+                root.Add(key, 1);
+
+                string json = JsonSerializer.Serialize(root);
+                Assert.Contains(expectedKeySerialized, json);
+            }
+
+            public static IEnumerable<object[]> DictionaryKeysWithSpecialCharacters =>
+                new List<object[]>
+                {
+                    new object[] { float.MaxValue, JsonSerializer.Serialize(float.MaxValue)  },
+                    new object[] { double.MaxValue, JsonSerializer.Serialize(double.MaxValue) },
+                    new object[] { DateTimeOffset.MaxValue, JsonSerializer.Serialize(DateTimeOffset.MaxValue) }
+                };
+
+            [Theory]
+            [MemberData(nameof(EscapedMemberData))]
+            public void TestEscapedValuesOnDeserialize(string escapedPropertyName, object expectedDictionaryKey, Type dictionaryType)
+            {
+                string json = $@"{{""{escapedPropertyName}"":1}}";
+                IDictionary root = (IDictionary)JsonSerializer.Deserialize(json, dictionaryType);
+
+                bool containsKey = root.Contains(expectedDictionaryKey);
+                Assert.True(containsKey);
+                Assert.Equal(1, root[expectedDictionaryKey]);
+            }
+
+            [Theory]
+            [MemberData(nameof(EscapedMemberData))]
+            public async Task TestEscapedValuesOnDeserializeAsync(string escapedPropertyName, object expectedDictionaryKey, Type dictionaryType)
+            {
+                string json = $@"{{""{escapedPropertyName}"":1}}";
+                MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
+                IDictionary root = (IDictionary)await JsonSerializer.DeserializeAsync(stream, dictionaryType);
+
+                bool containsKey = root.Contains(expectedDictionaryKey);
+                Assert.True(containsKey);
+                Assert.Equal(1, root[expectedDictionaryKey]);
+            }
+
+            public static IEnumerable<object[]> EscapedMemberData =>
+                new List<object[]>
+                {
+                    new object[] { @"\u0031\u0032\u0037",
+                        sbyte.MaxValue, typeof(Dictionary<sbyte, int>) },
+                    new object[] { @"\u0032\u0035\u0035",
+                        byte.MaxValue, typeof(Dictionary<byte, int>) },
+                    new object[] { @"\u0033\u0032\u0037\u0036\u0037",
+                        short.MaxValue, typeof(Dictionary<short, int>) },
+                    new object[] { @"\u0036\u0035\u0035\u0033\u0035",
+                        ushort.MaxValue, typeof(Dictionary<ushort, int>) },
+                    new object[] { @"\u0032\u0031\u0034\u0037\u0034\u0038\u0033\u0036\u0034\u0037",
+                        int.MaxValue, typeof(Dictionary<int, int>) },
+                    new object[] { @"\u0034\u0032\u0039\u0034\u0039\u0036\u0037\u0032\u0039\u0035",
+                        uint.MaxValue, typeof(Dictionary<uint, int>) },
+                    new object[] { @"\u0039\u0032\u0032\u0033\u0033\u0037\u0032\u0030\u0033\u0036\u0038\u0035\u0034\u0037\u0037\u0035\u0038\u0030\u0037",
+                        long.MaxValue, typeof(Dictionary<long, int>) },
+                    new object[] { @"\u0031\u0038\u0034\u0034\u0036\u0037\u0034\u0034\u0030\u0037\u0033\u0037\u0030\u0039\u0035\u0035\u0031\u0036\u0031\u0035",
+                        ulong.MaxValue, typeof(Dictionary<ulong, int>) },
+                    // Do not use max values on floating point types since it may have different string representations depending on the tfm.
+                    new object[] { @"\u0033\u002e\u0031\u0032\u0035\u0065\u0037",
+                        3.125e7f, typeof(Dictionary<float, int>) },
+                    new object[] { @"\u0033\u002e\u0031\u0032\u0035\u0065\u0037",
+                        3.125e7d, typeof(Dictionary<double, int>) },
+                    new object[] { @"\u0033\u002e\u0031\u0032\u0035\u0065\u0037",
+                        3.125e7m, typeof(Dictionary<decimal, int>) },
+                    new object[] { @"\u0039\u0039\u0039\u0039\u002d\u0031\u0032\u002d\u0033\u0031\u0054\u0032\u0033\u003a\u0035\u0039\u003a\u0035\u0039\u002e\u0039\u0039\u0039\u0039\u0039\u0039\u0039",
+                        DateTime.MaxValue, typeof(Dictionary<DateTime, int>) },
+                    new object[] { @"\u0039\u0039\u0039\u0039\u002d\u0031\u0032\u002d\u0033\u0031\u0054\u0032\u0033\u003a\u0035\u0039\u003a\u0035\u0039\u002e\u0039\u0039\u0039\u0039\u0039\u0039\u0039\u002b\u0030\u0030\u003a\u0030\u0030",
+                        DateTimeOffset.MaxValue, typeof(Dictionary<DateTimeOffset, int>) },
+                    new object[] { @"\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u002d\u0030\u0030\u0030\u0030\u002d\u0030\u0030\u0030\u0030\u002d\u0030\u0030\u0030\u0030\u002d\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0030",
+                        Guid.Empty, typeof(Dictionary<Guid, int>) },
+                    new object[] { @"\u0042\u0061\u0072",
+                        MyEnum.Bar, typeof(Dictionary<MyEnum, int>) },
+                    new object[] { @"\u0042\u0061\u0072\u002c\u0042\u0061\u007a",
+                        MyEnumFlags.Bar | MyEnumFlags.Baz, typeof(Dictionary<MyEnumFlags, int>) },
+                    new object[] { @"\u002b", '+', typeof(Dictionary<char, int>) }
+                };
+        }
+
+        public class MyPublicClass { }
+
+        public struct MyPublicStruct { }
+
+        public enum MyEnum
+        {
+            Foo,
+            Bar
+        }
+
+        [Flags]
+        public enum MyEnumFlags
+        {
+            Foo = 1,
+            Bar = 2,
+            Baz = 4
+        }
+
+        private class ClassWithIDictionary
+        {
+            public IDictionary Dictionary { get; set; }
+        }
+
+        private class ClassWithDictionary
+        {
+            public Dictionary<object, object> Dictionary { get; set; }
+        }
+
+        private class ClassWithExtensionData
+        {
+            [JsonExtensionData]
+            public Dictionary<int, object> Overflow { get; set; }
+        }
+
+        private class UnsupportedDictionaryWrapper
+        {
+            public Dictionary<int[], int> Dictionary { get; set; }
+        }
+
+        public class FixedNamingPolicy : JsonNamingPolicy
+        {
+            public const string FixedName = nameof(FixedName);
+            public override string ConvertName(string name) => FixedName;
+        }
+
+        public class SuffixNamingPolicy : JsonNamingPolicy
+        {
+            public const string Suffix = "_Suffix";
+            public override string ConvertName(string name) => name + Suffix;
+        }
+    }
+}
index bf1fb5a..9dd3c4d 100644 (file)
@@ -642,13 +642,6 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         [Fact]
-        public static void FirstGenericArgNotStringFail()
-        {
-            Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<Dictionary<int, int>>(@"{1:1}"));
-            Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<ImmutableDictionary<int, int>>(@"{1:1}"));
-        }
-
-        [Fact]
         public static void DictionaryOfList()
         {
             const string JsonString = @"{""Key1"":[1,2],""Key2"":[3,4]}";
@@ -1619,8 +1612,7 @@ namespace System.Text.Json.Serialization.Tests
             NotSupportedException ex = Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<ClassWithNotSupportedDictionary>(json));
 
             // The exception contains the type.
-            Assert.Contains(typeof(Dictionary<int, int>).ToString(), ex.Message);
-            Assert.DoesNotContain("Path: ", ex.Message);
+            Assert.Contains(typeof(Dictionary<int[,], int>).ToString(), ex.Message);
         }
 
         [Fact]
@@ -1840,12 +1832,12 @@ namespace System.Text.Json.Serialization.Tests
 
         public class ClassWithNotSupportedDictionary
         {
-            public Dictionary<int, int> MyDictionary { get; set; }
+            public Dictionary<int[,], int> MyDictionary { get; set; }
         }
 
         public class ClassWithNotSupportedDictionaryButIgnored
         {
-            [JsonIgnore] public Dictionary<int, int> MyDictionary { get; set; }
+            [JsonIgnore] public Dictionary<int[,], int> MyDictionary { get; set; }
         }
 
         public class AllSingleUpperPropertiesParent
@@ -2143,15 +2135,6 @@ namespace System.Text.Json.Serialization.Tests
             Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(dictionary));
         }
 
-
-        [Fact]
-        public static void VerifyIDictionaryWithNonStringKey()
-        {
-            IDictionary dictionary = new Hashtable();
-            dictionary.Add(1, "value");
-            Assert.Throws<JsonException>(() => JsonSerializer.Serialize(dictionary));
-        }
-
         private class ClassWithoutParameterlessCtor
         {
             public ClassWithoutParameterlessCtor(int num) { }
index 3e9eae8..7034054 100644 (file)
@@ -1148,10 +1148,10 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         [Fact]
-        public static void IReadOnlyDictionary_NonStringKey_NotSupported()
+        public static void IReadOnlyDictionary_NotSupportedKey()
         {
-            Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<IReadOnlyDictionary<int, int>>(""));
-            Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(new GenericIReadOnlyDictionaryWrapper<int, int>()));
+            Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<IReadOnlyDictionary<Uri, int>>(@"{""http://foo"":1}"));
+            Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(new GenericIReadOnlyDictionaryWrapper<Uri, int>(new Dictionary<Uri, int> { { new Uri("http://foo"), 1 } })));
         }
     }
 }
index 37b2765..b97696d 100644 (file)
@@ -76,7 +76,7 @@ namespace System.Text.Json.Serialization.Tests
             {
                 DictionaryWrapper = new UnsupportedDictionaryWrapper()
             };
-            wrapper.DictionaryWrapper[1] = 1;
+            wrapper.DictionaryWrapper[new int[,] { }] = 1;
 
             // Without converter, we throw.
             Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<UnsupportedDerivedTypesWrapper_Dictionary>(json));
@@ -128,7 +128,7 @@ namespace System.Text.Json.Serialization.Tests
 
     public class DictionaryWrapper : Dictionary<string, int> { }
 
-    public class UnsupportedDictionaryWrapper : Dictionary<int, int> { }
+    public class UnsupportedDictionaryWrapper : Dictionary<int[,], int> { }
 
     public class DerivedTypesWrapper
     {
index 8c95c47..73906d8 100644 (file)
@@ -866,9 +866,8 @@ namespace System.Text.Json.Serialization.Tests
             ClassWithInvalidExtensionPropertyStringString obj1 = new ClassWithInvalidExtensionPropertyStringString();
             Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(obj1));
 
-            // This fails with NotSupportedException since all Dictionaries currently need to have a string TKey.
             ClassWithInvalidExtensionPropertyObjectString obj2 = new ClassWithInvalidExtensionPropertyObjectString();
-            Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(obj2));
+            Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(obj2));
         }
 
         private class ClassWithExtensionPropertyAlreadyInstantiated
index c2fd214..8fb2731 100644 (file)
@@ -851,18 +851,41 @@ namespace System.Text.Json.Serialization.Tests
             JsonSerializerOptions options = new JsonSerializerOptions();
 
             // Unsupported collections will throw on serialize by default.
-            Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(new ClassWithUnsupportedDictionary(), options));
+            // Only when the collection contains elements.
 
-            // Unsupported collections will throw on deserialize by default.
+            var dictionary = new Dictionary<object, object>();
+            // Uri is an unsupported dictionary key.
+            dictionary.Add(new Uri("http://foo"), "bar");
+
+            var concurrentDictionary = new ConcurrentDictionary<object, object>(dictionary);
+
+            var instance = new ClassWithUnsupportedDictionary()
+            {
+                MyConcurrentDict = concurrentDictionary,
+                MyIDict = dictionary
+            };
+
+            var instanceWithIgnore = new ClassWithIgnoredUnsupportedDictionary
+            {
+                MyConcurrentDict = concurrentDictionary,
+                MyIDict = dictionary
+            };
+
+            Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(instance, options));
+
+            // Unsupported collections will throw on deserialize by default if they contain elements.
             options = new JsonSerializerOptions();
             Assert.Throws<NotSupportedException>(() => JsonSerializer.Deserialize<WrapperForClassWithUnsupportedDictionary>(wrapperJson, options));
 
             options = new JsonSerializerOptions();
-            // Unsupported collections will throw on serialize by default.
-            Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(new WrapperForClassWithUnsupportedDictionary(), options));
+            // Unsupported collections will throw on serialize by default if they contain elements.
+            Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(instance, options));
 
             // When ignored, we can serialize and deserialize without exceptions.
             options = new JsonSerializerOptions();
+
+            Assert.NotNull(JsonSerializer.Serialize(instanceWithIgnore, options));
+
             ClassWithIgnoredUnsupportedDictionary obj = JsonSerializer.Deserialize<ClassWithIgnoredUnsupportedDictionary>(json, options);
             Assert.Null(obj.MyDict);
 
index ddfccac..918787c 100644 (file)
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFrameworks>$(NetCoreAppCurrent);$(NetFrameworkCurrent)</TargetFrameworks>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -41,6 +41,7 @@
     <Compile Include="Serialization\CollectionTests\CollectionTests.Concurrent.cs" />
     <Compile Include="Serialization\CollectionTests\CollectionTests.Dictionary.cs" />
     <Compile Include="Serialization\CollectionTests\CollectionTests.Dictionary.KeyPolicy.cs" />
+    <Compile Include="Serialization\CollectionTests\CollectionTests.Dictionary.NonStringKey.cs" />
     <Compile Include="Serialization\CollectionTests\CollectionTests.Generic.Read.cs" />
     <Compile Include="Serialization\CollectionTests\CollectionTests.Generic.Write.cs" />
     <Compile Include="Serialization\CollectionTests\CollectionTests.Immutable.Read.cs" />