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