diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs index 109ecd539a95b..b844bd6a731de 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs @@ -24,6 +24,7 @@ internal class EnumConverter : JsonConverter private readonly JsonNamingPolicy? _namingPolicy; private readonly ConcurrentDictionary _nameCache; + private ConcurrentDictionary? _sourceNameCache; // This is used to prevent flooding the cache due to exponential bitwise combinations of flags. // Since multiple threads can add to the cache, a few more values might be added. @@ -67,6 +68,14 @@ public EnumConverter(EnumConverterOptions converterOptions, JsonNamingPolicy? na namingPolicy == null ? JsonEncodedText.Encode(name, encoder) : FormatEnumValue(name, encoder)); + + if (namingPolicy != null) + { + if (_sourceNameCache == null) + _sourceNameCache = new ConcurrentDictionary(); + + _sourceNameCache.TryAdd(FormatEnumValueToString(name, null), name); + } } } @@ -278,14 +287,33 @@ private JsonEncodedText FormatEnumValue(string value, JavaScriptEncoder? encoder return JsonEncodedText.Encode(formatted, encoder); } - private string FormatEnumValueToString(string value, JavaScriptEncoder? encoder) + private string FormatNamingPolicy(string value, bool isWrite) + { + if (isWrite) + { + Debug.Assert(_namingPolicy != null); + return _namingPolicy.ConvertName(value); + } + else + { + Debug.Assert(_sourceNameCache != null); + if (_sourceNameCache.ContainsKey(value)) + return _sourceNameCache[value]; + + ThrowHelper.ThrowJsonException(); + } + + return default; + } + + private string FormatEnumValueToString(string value, JavaScriptEncoder? encoder, bool isWrite = true) { Debug.Assert(_namingPolicy != null); string converted; if (!value.Contains(ValueSeparator)) { - converted = _namingPolicy.ConvertName(value); + converted = FormatNamingPolicy(value, isWrite); } else { @@ -300,7 +328,7 @@ private string FormatEnumValueToString(string value, JavaScriptEncoder? encoder) for (int i = 0; i < enumValues.Length; i++) { - enumValues[i] = _namingPolicy.ConvertName(enumValues[i]); + enumValues[i] = FormatNamingPolicy(enumValues[i], isWrite); } converted = string.Join(ValueSeparator, enumValues); @@ -313,6 +341,9 @@ internal override T ReadWithQuotes(ref Utf8JsonReader reader) { string? enumString = reader.GetString(); + if (_sourceNameCache != null && enumString != null) + enumString = FormatEnumValueToString(enumString, null, false); + // Try parsing case sensitive first if (!Enum.TryParse(enumString, out T value) && !Enum.TryParse(enumString, ignoreCase: true, out value)) diff --git a/src/libraries/System.Text.Json/tests/Serialization/EnumConverterWithNamingPolicyTests.cs b/src/libraries/System.Text.Json/tests/Serialization/EnumConverterWithNamingPolicyTests.cs new file mode 100644 index 0000000000000..c26948d00656b --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/EnumConverterWithNamingPolicyTests.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace System.Text.Json.Serialization.Tests +{ + public class EnumConverterWithNamingPolicyTests + { + private readonly ITestOutputHelper _outputHelper; + + public EnumConverterWithNamingPolicyTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + public class SnakeCaseNamingPolicy : JsonNamingPolicy + { + public override string ConvertName(string name) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + var result = new StringBuilder(); + for (var i = 0; i < name.Length; i++) + { + var c = name[i]; + if (i == 0) + { + result.Append(char.ToLower(c)); + } + else + { + if (char.IsUpper(c)) + { + result.Append('_'); + result.Append(char.ToLower(c)); + } + else + { + result.Append(c); + } + } + } + return result.ToString(); + } + + } + + [Flags] + public enum TestType + { + None, + ValueOne, + ValueTwo, + } + + public class ObjectWithEnumProperty + { + public TestType TestType { get; set; } + } + + [Fact] + public void TestEnumCase() + { + var namingPolicy = new SnakeCaseNamingPolicy(); + + var opts = new JsonSerializerOptions() + { + PropertyNamingPolicy = namingPolicy, + DictionaryKeyPolicy = namingPolicy, + Converters = + { + new JsonStringEnumConverter(namingPolicy) + } + }; + + var enumValues = Enum.GetValues(typeof(TestType)).Cast(); + foreach (var v in enumValues) + { + var sourceObject = new ObjectWithEnumProperty() + { + TestType = v + }; + + var json = JsonSerializer.Serialize(sourceObject, opts); + _outputHelper.WriteLine(json); + var deserializedObject = JsonSerializer.Deserialize(json, opts); + Assert.Equal(sourceObject.TestType, deserializedObject.TestType); + } + } + + + [Fact] + public void TestFlagsEnumValuesWithNamingPolicy() + { + var namingPolicy = new SnakeCaseNamingPolicy(); + + var opts = new JsonSerializerOptions() + { + PropertyNamingPolicy = namingPolicy, + DictionaryKeyPolicy = namingPolicy, + Converters = + { + new JsonStringEnumConverter(namingPolicy) + } + }; + + var sourceObject = new ObjectWithEnumProperty() + { + TestType = TestType.ValueOne | TestType.ValueTwo + }; + + var json = JsonSerializer.Serialize(sourceObject, opts); + _outputHelper.WriteLine(json); + + Assert.Equal(@"{""test_type"":""value_one, value_two""}", json); + + var restoredObject = JsonSerializer.Deserialize(json, opts); + + Assert.Equal(sourceObject.TestType, restoredObject.TestType); + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj index 6e1e66c690248..01444d4da0670 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -88,6 +88,7 @@ +