Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Honor propery naming policy when (de)serializing KeyValuePair instances #1198

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/libraries/System.Text.Json/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,9 @@
<data name="SerializerPropertyNameNull" xml:space="preserve">
<value>The JSON property name for '{0}.{1}' cannot be null.</value>
</data>
<data name="SerializerPropertyNamingPolicyReturnNull" xml:space="preserve">
<value>The property naming policy '{0}' cannot return a null property name.</value>
</data>
<data name="DeserializeDuplicateKey" xml:space="preserve">
<value>An item with the same property name '{0}' has already been added.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,43 @@ namespace System.Text.Json.Serialization.Converters
{
internal sealed class JsonKeyValuePairConverter : JsonConverterFactory
{
private readonly byte[] _keyName;
private readonly byte[] _valueName;

private readonly JsonEncodedText _encodedKeyName;
private readonly JsonEncodedText _encodedValueName;

public JsonKeyValuePairConverter(JsonSerializerOptions options)
{
JsonNamingPolicy namingPolicy = options.PropertyNamingPolicy;
if (namingPolicy == null)
{
_keyName = new byte[] { (byte)'K', (byte)'e', (byte)'y' };
_valueName = new byte[] { (byte)'V', (byte)'a', (byte)'l', (byte)'u', (byte)'e' };
}
else
{
string propertyName = namingPolicy.ConvertName("Key");
if (propertyName == null)
{
ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNamingPolicyReturnNull(namingPolicy);
}
_keyName = JsonReaderHelper.s_utf8Encoding.GetBytes(propertyName);

propertyName = namingPolicy.ConvertName("Value");
if (propertyName == null)
{
ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNamingPolicyReturnNull(namingPolicy);
}
_valueName = JsonReaderHelper.s_utf8Encoding.GetBytes(propertyName);
}

// "encoder: null" is used since the literal values of "Key" and "Value" should not normally be escaped
// unless a custom encoder is used that escapes these ASCII characters (rare).
_encodedKeyName = JsonEncodedText.Encode(_keyName, encoder: null);
_encodedValueName = JsonEncodedText.Encode(_valueName, encoder: null);
}

public override bool CanConvert(Type typeToConvert)
{
if (!typeToConvert.IsGenericType)
Expand All @@ -19,7 +56,9 @@ public override bool CanConvert(Type typeToConvert)
return (generic == typeof(KeyValuePair<,>));
}

[PreserveDependency(".ctor()", "System.Text.Json.Serialization.Converters.JsonKeyValuePairConverter`2")]
[PreserveDependency(
".ctor(Byte[], Byte[], System.Text.Json.JsonEncodedText, System.Text.Json.JsonEncodedText)",
"System.Text.Json.Serialization.Converters.JsonKeyValuePairConverter`2")]
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
{
Type keyType = type.GetGenericArguments()[0];
Expand All @@ -29,7 +68,7 @@ public override JsonConverter CreateConverter(Type type, JsonSerializerOptions o
typeof(JsonKeyValuePairConverter<,>).MakeGenericType(new Type[] { keyType, valueType }),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: null,
args: new object[] { _keyName, _valueName, _encodedKeyName, _encodedValueName },
culture: null);

return converter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ namespace System.Text.Json.Serialization.Converters
{
internal sealed class JsonKeyValuePairConverter<TKey, TValue> : JsonConverter<KeyValuePair<TKey, TValue>>
{
private const string KeyName = "Key";
private const string ValueName = "Value";
private readonly byte[] _keyName;
private readonly byte[] _valueName;

// "encoder: null" is used since the literal values of "Key" and "Value" should not normally be escaped
// unless a custom encoder is used that escapes these ASCII characters (rare).
// Also by not specifying an encoder allows the values to be cached statically here.
private static readonly JsonEncodedText _keyName = JsonEncodedText.Encode(KeyName, encoder: null);
private static readonly JsonEncodedText _valueName = JsonEncodedText.Encode(ValueName, encoder: null);
private readonly JsonEncodedText _encodedKeyName;
private readonly JsonEncodedText _encodedValueName;

public JsonKeyValuePairConverter(byte[] keyName, byte[] valueName, JsonEncodedText encodedKeyName, JsonEncodedText encodedValueName)
{
_keyName = keyName;
_valueName = valueName;
_encodedKeyName = encodedKeyName;
_encodedValueName = encodedValueName;
}

public override KeyValuePair<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Expand All @@ -37,13 +42,12 @@ public override KeyValuePair<TKey, TValue> Read(ref Utf8JsonReader reader, Type
ThrowHelper.ThrowJsonException();
}

string propertyName = reader.GetString();
if (propertyName == KeyName)
if (reader.ValueTextEquals(_keyName))
{
k = ReadProperty<TKey>(ref reader, typeToConvert, options);
keySet = true;
}
else if (propertyName == ValueName)
else if (reader.ValueTextEquals(_valueName))
{
v = ReadProperty<TValue>(ref reader, typeToConvert, options);
valueSet = true;
Expand All @@ -60,13 +64,12 @@ public override KeyValuePair<TKey, TValue> Read(ref Utf8JsonReader reader, Type
ThrowHelper.ThrowJsonException();
}

propertyName = reader.GetString();
if (propertyName == ValueName)
if (reader.ValueTextEquals(_valueName))
{
v = ReadProperty<TValue>(ref reader, typeToConvert, options);
valueSet = true;
}
else if (propertyName == KeyName)
else if (reader.ValueTextEquals(_keyName))
{
k = ReadProperty<TKey>(ref reader, typeToConvert, options);
keySet = true;
Expand Down Expand Up @@ -131,8 +134,8 @@ private void WriteProperty<T>(Utf8JsonWriter writer, T value, JsonEncodedText na
public override void Write(Utf8JsonWriter writer, KeyValuePair<TKey, TValue> value, JsonSerializerOptions options)
{
writer.WriteStartObject();
WriteProperty(writer, value.Key, _keyName, options);
WriteProperty(writer, value.Value, _valueName, options);
WriteProperty(writer, value.Key, _encodedKeyName, options);
WriteProperty(writer, value.Value, _encodedValueName, options);
writer.WriteEndObject();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ protected override void OnWriteDictionary(ref WriteStackFrame current, Utf8JsonW

if (key == null)
{
ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(Options.DictionaryKeyPolicy.GetType());
ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(Options.DictionaryKeyPolicy);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ internal static void WriteDictionary<TProperty>(

if (key == null)
{
ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(options.DictionaryKeyPolicy.GetType());
ThrowHelper.ThrowInvalidOperationException_SerializerDictionaryKeyNull(options.DictionaryKeyPolicy);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,28 @@ namespace System.Text.Json
/// </summary>
public sealed partial class JsonSerializerOptions
{
private List<JsonConverter> _defaultFactoryConverters;

// The global list of built-in simple converters.
private static readonly Dictionary<Type, JsonConverter> s_defaultSimpleConverters = GetDefaultSimpleConverters();

// The global list of built-in converters that override CanConvert().
private static readonly List<JsonConverter> s_defaultFactoryConverters = GetDefaultConverters();

// The cached converters (custom or built-in).
private readonly ConcurrentDictionary<Type, JsonConverter> _converters = new ConcurrentDictionary<Type, JsonConverter>();

// The global list of built-in converters that override CanConvert().
private List<JsonConverter> DefaultFactoryConverters
{
get
{
if (_defaultFactoryConverters == null)
{
_defaultFactoryConverters = GetDefaultConverters();
}

return _defaultFactoryConverters;
}
}

private static Dictionary<Type, JsonConverter> GetDefaultSimpleConverters()
{
var converters = new Dictionary<Type, JsonConverter>(NumberOfSimpleConverters);
Expand All @@ -40,15 +53,15 @@ private static Dictionary<Type, JsonConverter> GetDefaultSimpleConverters()
return converters;
}

private static List<JsonConverter> GetDefaultConverters()
private List<JsonConverter> GetDefaultConverters()
{
const int NumberOfConverters = 2;

var converters = new List<JsonConverter>(NumberOfConverters);

// Use a list for converters that implement CanConvert().
converters.Add(new JsonConverterEnum());
converters.Add(new JsonKeyValuePairConverter());
converters.Add(new JsonKeyValuePairConverter(this));

// We will likely add collection converters here in the future.

Expand Down Expand Up @@ -140,7 +153,7 @@ public JsonConverter GetConverter(Type typeToConvert)
}
else
{
foreach (JsonConverter item in s_defaultFactoryConverters)
foreach (JsonConverter item in DefaultFactoryConverters)
{
if (item.CanConvert(typeToConvert))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,15 @@ public static void ThrowInvalidOperationException_SerializerPropertyNameNull(Typ
throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNameNull, parentType, jsonPropertyInfo.PropertyInfo.Name));
}

public static void ThrowInvalidOperationException_SerializerPropertyNamingPolicyReturnNull(JsonNamingPolicy namingPolicy)
{
throw new InvalidOperationException(SR.Format(SR.SerializerPropertyNamingPolicyReturnNull, namingPolicy.GetType()));
}

[MethodImpl(MethodImplOptions.NoInlining)]
public static void ThrowInvalidOperationException_SerializerDictionaryKeyNull(Type policyType)
public static void ThrowInvalidOperationException_SerializerDictionaryKeyNull(JsonNamingPolicy namingPolicy)
{
throw new InvalidOperationException(SR.Format(SR.SerializerDictionaryKeyNull, policyType));
throw new InvalidOperationException(SR.Format(SR.SerializerDictionaryKeyNull, namingPolicy.GetType()));
}

[MethodImpl(MethodImplOptions.NoInlining)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,112 @@ public static void LongPropertyNames(int propertyLength, char ch)
string jsonRoundTripped = JsonSerializer.Serialize(obj, options);
Assert.Equal(json, jsonRoundTripped);
}

[Fact]
public static void RootKeyValuePairSerialize()
{
JsonSerializerOptions options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

const string jsonWithoutPolicy = @"{""Key"":""MyKey"",""Value"":""MyValue""}";
const string jsonWithPolicy = @"{""key"":""MyKey"",""value"":""MyValue""}";

// Baseline: Without policy.
string serialized = JsonSerializer.Serialize(new KeyValuePair<string, string>("MyKey", "MyValue"));
Assert.Equal(jsonWithoutPolicy, serialized);

KeyValuePair<string, string> kvp = JsonSerializer.Deserialize<KeyValuePair<string, string>>(serialized);
Assert.Equal("MyKey", kvp.Key);
Assert.Equal("MyValue", kvp.Value);

// No property name match.
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<KeyValuePair<string, string>>(jsonWithPolicy));

// With policy.
serialized = JsonSerializer.Serialize(new KeyValuePair<string, string>("MyKey", "MyValue"), options);
Assert.Equal(jsonWithPolicy, serialized);

kvp = JsonSerializer.Deserialize<KeyValuePair<string, string>>(serialized, options);
Assert.Equal("MyKey", kvp.Key);
Assert.Equal("MyValue", kvp.Value);

// No property name match.
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<KeyValuePair<string, string>>(jsonWithoutPolicy, options));
}

[Fact]
public static void KeyValuePairAsCollectionElementSerialize()
{
JsonSerializerOptions options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

const string jsonWithoutPolicy = @"[{""Key"":""MyKey"",""Value"":""MyValue""}]";
const string jsonWithPolicy = @"[{""key"":""MyKey"",""value"":""MyValue""}]";

// Baseline: Without policy.
string serialized = JsonSerializer.Serialize(new KeyValuePair<string, string>[] { new KeyValuePair<string, string>("MyKey", "MyValue") });
Assert.Equal(jsonWithoutPolicy, serialized);

KeyValuePair<string, string>[] arr = JsonSerializer.Deserialize<KeyValuePair<string, string>[]>(serialized);
Assert.Equal("MyKey", arr[0].Key);
Assert.Equal("MyValue", arr[0].Value);

// No property name match.
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<KeyValuePair<string, string>[]>(jsonWithPolicy));

// With policy.
serialized = JsonSerializer.Serialize(new KeyValuePair<string, string>[] { new KeyValuePair<string, string>("MyKey", "MyValue") }, options);
Assert.Equal(jsonWithPolicy, serialized);

arr = JsonSerializer.Deserialize<KeyValuePair<string, string>[]>(serialized, options);
Assert.Equal("MyKey", arr[0].Key);
Assert.Equal("MyValue", arr[0].Value);

// No property name match.
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<KeyValuePair<string, string>[]>(jsonWithoutPolicy, options));
}

[Fact]
public static void KeyValuePairAsClassPropertyElementSerialize()
{
JsonSerializerOptions options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

const string jsonWithoutPolicy = @"{""Kvp"":{""Key"":""MyKey"",""Value"":""MyValue""}}";
const string jsonWithPolicy = @"{""kvp"":{""key"":""MyKey"",""value"":""MyValue""}}";

// Baseline: Without policy.
string serialized = JsonSerializer.Serialize(new ClassWithKVP { Kvp = new KeyValuePair<string, string>("MyKey", "MyValue") });
Assert.Equal(jsonWithoutPolicy, serialized);

ClassWithKVP obj = JsonSerializer.Deserialize<ClassWithKVP>(serialized);
Assert.Equal("MyKey", obj.Kvp.Key);
Assert.Equal("MyValue", obj.Kvp.Value);

// No property name match.
obj = JsonSerializer.Deserialize<ClassWithKVP>(jsonWithPolicy);
Assert.Null(obj.Kvp.Key);
Assert.Null(obj.Kvp.Value);

// With policy.
serialized = JsonSerializer.Serialize(new ClassWithKVP { Kvp = new KeyValuePair<string, string>("MyKey", "MyValue") }, options);
Assert.Equal(jsonWithPolicy, serialized);

obj = JsonSerializer.Deserialize<ClassWithKVP>(serialized, options);
Assert.Equal("MyKey", obj.Kvp.Key);
Assert.Equal("MyValue", obj.Kvp.Value);

// No property name match.
obj = JsonSerializer.Deserialize<ClassWithKVP>(jsonWithoutPolicy, options);
Assert.Null(obj.Kvp.Key);
Assert.Null(obj.Kvp.Value);
}
}

public class OverridePropertyNameDesignTime_TestClass
Expand Down Expand Up @@ -495,4 +601,9 @@ public class EmptyClassWithExtensionProperty
[JsonExtensionData]
public IDictionary<string, JsonElement> MyOverflow { get; set; }
}

public class ClassWithKVP
{
public KeyValuePair<string, string> Kvp { get; set; }
}
}