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)));
+ }
}
}
}