-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
fix reading json with naming policy #42302
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,9 @@ internal class EnumConverter<T> : JsonConverter<T> | |
private readonly JsonNamingPolicy? _namingPolicy; | ||
|
||
private readonly ConcurrentDictionary<ulong, JsonEncodedText> _nameCache; | ||
private readonly ConcurrentDictionary<JsonEncodedText, string> _sourceNameCache; | ||
private readonly bool _needRestoreSourceName; | ||
private readonly JsonSerializerOptions _serializerOptions; | ||
|
||
// 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. | ||
|
@@ -43,7 +46,9 @@ public EnumConverter(EnumConverterOptions converterOptions, JsonNamingPolicy? na | |
{ | ||
_converterOptions = converterOptions; | ||
_namingPolicy = namingPolicy; | ||
_serializerOptions = serializerOptions; | ||
_nameCache = new ConcurrentDictionary<ulong, JsonEncodedText>(); | ||
_sourceNameCache = new ConcurrentDictionary<JsonEncodedText, string>(); | ||
|
||
string[] names = Enum.GetNames(TypeToConvert); | ||
Array values = Enum.GetValues(TypeToConvert); | ||
|
@@ -67,7 +72,12 @@ public EnumConverter(EnumConverterOptions converterOptions, JsonNamingPolicy? na | |
namingPolicy == null | ||
? JsonEncodedText.Encode(name, encoder) | ||
: FormatEnumValue(name, encoder)); | ||
|
||
if (namingPolicy != null) | ||
_sourceNameCache.TryAdd(FormatEnumValue(name, encoder), name); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This won't work for |
||
} | ||
|
||
_needRestoreSourceName = namingPolicy != null; | ||
} | ||
|
||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
|
@@ -313,6 +323,9 @@ internal override T ReadWithQuotes(ref Utf8JsonReader reader) | |
{ | ||
string? enumString = reader.GetString(); | ||
|
||
if (_needRestoreSourceName && enumString != null) | ||
_sourceNameCache.TryGetValue(FormatEnumValue(enumString, _serializerOptions.Encoder), out enumString); | ||
|
||
// Try parsing case sensitive first | ||
if (!Enum.TryParse(enumString, out T value) | ||
&& !Enum.TryParse(enumString, ignoreCase: true, out value)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
// 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(); | ||
} | ||
|
||
} | ||
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<TestType>(); | ||
foreach (var v in enumValues) | ||
{ | ||
var sourceObject = new ObjectWithEnumProperty() | ||
{ | ||
TestType = v | ||
}; | ||
|
||
var json = JsonSerializer.Serialize(sourceObject, opts); | ||
_outputHelper.WriteLine(json); | ||
var deserializedObject = JsonSerializer.Deserialize<ObjectWithEnumProperty>(json, opts); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If [Theory]
[InlineData(@"""none""", TestType.None)]
[InlineData(@"0", TestType.None)]
[InlineData(@"""value_one""", TestType.ValueOne)]
[InlineData(@"1", TestType.ValueOne)]
[InlineData(@"""value_two""", TestType.ValueTwo)]
[InlineData(@"2", TestType.ValueTwo)]
public void TestEnumNamingPolicy(string json, TestType expectedValue)
{
// ...opts as above
string json = $@"{{ ""test_type"": {json} }}";
var deserializedObject = JsonSerializer.Deserialize<ObjectWithEnumProperty>(json, opts);
Assert.Equal(expectedValue, deserializedObject.TestType);
} I ask because it appears that the cache for enum names is only ever populated if you write the value first. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cache is initialized by EnumConverter constructor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, I was unable to get it work pass locally, looks like it may be an issue on my end! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sory, i was wrong, cache initializer will be for first 64 enum values. |
||
Assert.Equal(sourceObject.TestType, deserializedObject.TestType); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider initializing this field only when required, i.e:
namingPolicy
not null and the enum contain any names.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be better to use the encoded string version of the enum value as
TKey
and the enum value asTValue
so when you callTryGetValue
while reading you don't have to create the JsonEncodedText which IIRC is expensive.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the code review.
and update this pull request.
I just don't know what to do in cases where the NameCacheSizeSoftLimit = 64 restriction occurs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And I'll try to see how it is done in Newtonsoft.Json StringEnumConverter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Newtonsoft.Json create cache by all enum names, maybe there is nothing dangerous when once (in constructor or static code) a cache is created for a type for all its values?
Is there a danger of a cache overflow only if it is added to the cache at the user's request (for example, write json)?
Current version of type EnumConverter contains two point adding values to cache: 1) constructor and 2) method Write(...). I think that NameCacheSizeSoftLimit added when cache writes on method Write(...).
Maybe adding cache values in method's write & writequotes not actual? And check NameCacheSizeSoftLimit in constructor not actual?
runtime/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
Line 388 in b8a6677
runtime/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
Line 199 in b8a6677
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realized that adding to the cache in Write(...) methods is required in the case of flags when intermediate values.
However, in the constructor, it may not be worth checking the NameCacheSizeSoftLimit?
runtime/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
Line 58 in b8a6677
Although, for sure, there are no such large enums, so we can assume that size 64 is enough for everyone.