-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Add logic to properly honor naming policy when serializing flag enums #36726
Conversation
...raries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
Outdated
Show resolved
Hide resolved
...raries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
Outdated
Show resolved
Hide resolved
...raries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
Show resolved
Hide resolved
Since naming policies are only one-way the resulting JSON that had a naming policy applied may not be able to be deserialized unless we also apply the naming policy to the CLR enum value names during deserialization (and match them up that way -- like we do with properties). |
We already apply the naming policy when serializing, just incorrectly in the case of flags. This change doesn't prohibit any deserialization that was possible prior: Serializing Before: "compressed, IntegrityStream" Round tripping is not broken here because we do a case insensitive read with Enum.Parse. Serializing Before: "compressed, _integrity_stream" Neither of these strings can be read by Enum.Parse, as it expects some ordering of "Compressed, IntegrityStream". So, we are not breaking deserialization here, as it never worked for this scenario. Ultimately, correctly applying the naming won't affect reading, given the current deserialization implementation. The change is a prerequisite if we want to recognize the policy during deserialization (#31619) to enable roundtripping when a naming policy is used (one which does more than return a case-insensitive match). |
That works with camel-casing but not with snake-casing... |
Yes, but we already honor snake-casing when serializing, and then can't deserialize the output: JsonSerializerOptions options = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter(namingPolicy: new SimpleSnakeCasePolicy())} };
FileAttributes val = FileAttributes.IntegrityStream;
string json = JsonSerializer.Serialize(val, options);
Console.WriteLine(json); // "integrity_stream"
JsonSerializer.Deserialize<FileAttributes>(json, options); // JsonException I'll reach out to discuss this offline. |
...raries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
Outdated
Show resolved
Hide resolved
…naming policy is not used
Benchmarks for this change show serialization when a naming policy is used is up to ~2.48x faster when we have long enum representation. We also have less allocs (up to ~47% decrease) in these scenarios. This is due to creating and caching the Before
After
|
...raries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
Outdated
Show resolved
Hide resolved
...raries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
Outdated
Show resolved
Hide resolved
...raries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
Outdated
Show resolved
Hide resolved
...raries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
Outdated
Show resolved
Hide resolved
...raries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
Outdated
Show resolved
Hide resolved
...raries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
Outdated
Show resolved
Hide resolved
Updated benchmark results following latest changes:
|
…, and add more tests
@@ -16,32 +18,57 @@ internal class EnumConverter<T> : JsonConverter<T> | |||
// Odd type codes are conveniently signed types (for enum backing types). | |||
private static readonly string? s_negativeSign = ((int)s_enumTypeCode % 2) == 0 ? null : NumberFormatInfo.CurrentInfo.NegativeSign; |
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's incorrect to cache any thread-local state (such as information about the thread-current culture) into a global static. The end result of this is that the very first time this code is executed, the executing thread's negative sign will be read and will be applied to all subsequent operations, regardless of what culture the other threads are running as.
If you wanted this to be properly culture-aware, you need to query the current thread's negative sign on every single call to IsValidIdentifier
.
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.
This would be incorrect even if it was a thread-static, of course.
LGTM, thanks! :) |
Fixes #31622.