String enum converter (dotnet/corefx#38702)
authorJeremy Kuhne <jeremy.kuhne@microsoft.com>
Thu, 20 Jun 2019 19:07:20 +0000 (12:07 -0700)
committerGitHub <noreply@github.com>
Thu, 20 Jun 2019 19:07:20 +0000 (12:07 -0700)
* String enum converter

Adds public converter for converting enums to strings and vice-versa.

* Address feedback.

* Missing a readonly

* Fix exception and swtich to ConcurrentDictionary

* Merge fixup

Commit migrated from https://github.com/dotnet/corefx/commit/66a18944db6d64b67adb1320d41004a9140babc1

src/libraries/System.Text.Json/ref/System.Text.Json.cs
src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/EnumConverterOptions.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonConverterEnum.cs [moved from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonEnumConverter.cs with 81% similarity]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonValueConverterEnum.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonDefaultNamingPolicy.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonNamingPolicy.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonStringEnumConverter.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/Serialization/EnumConverterTests.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj

index e1f0bc5..3c0482c 100644 (file)
@@ -442,6 +442,12 @@ namespace System.Text.Json.Serialization
         public virtual void Write(System.Text.Json.Utf8JsonWriter writer, T value, System.Text.Json.JsonEncodedText propertyName, System.Text.Json.JsonSerializerOptions options) { }
         public abstract void Write(System.Text.Json.Utf8JsonWriter writer, T value, System.Text.Json.JsonSerializerOptions options);
     }
+    public sealed class JsonStringEnumConverter : System.Text.Json.Serialization.JsonConverterFactory
+    {
+        public JsonStringEnumConverter(System.Text.Json.JsonNamingPolicy namingPolicy = null, bool allowIntegerValues = true) { }
+        public override bool CanConvert(System.Type type) { throw null; }
+        protected override System.Text.Json.Serialization.JsonConverter CreateConverter(System.Type type) { throw null; }
+    }
     [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false)]
     public sealed partial class JsonExtensionDataAttribute : System.Text.Json.Serialization.JsonAttribute
     {
index 9298c9f..6160807 100644 (file)
@@ -23,6 +23,9 @@
     <Compile Include="System\Text\Json\JsonHelpers.cs" />
     <Compile Include="System\Text\Json\JsonHelpers.Date.cs" />
     <Compile Include="System\Text\Json\JsonTokenType.cs" />
+    <Compile Include="System\Text\Json\Serialization\Converters\EnumConverterOptions.cs" />
+    <Compile Include="System\Text\Json\Serialization\JsonStringEnumConverter.cs" />
+    <Compile Include="System\Text\Json\Serialization\JsonDefaultNamingPolicy.cs" />
     <Compile Include="System\Text\Json\ThrowHelper.cs" />
     <Compile Include="System\Text\Json\ThrowHelper.Serialization.cs" />
     <Compile Include="System\Text\Json\Document\JsonDocument.cs" />
@@ -55,7 +58,6 @@
     <Compile Include="System\Text\Json\Serialization\Converters\DefaultIDictionaryConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\DefaultImmutableEnumerableConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\DefaultImmutableDictionaryConverter.cs" />
-    <Compile Include="System\Text\Json\Serialization\Converters\JsonEnumConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\JsonKeyValuePairConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\JsonValueConverterBoolean.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\JsonValueConverterByte.cs" />
@@ -65,6 +67,7 @@
     <Compile Include="System\Text\Json\Serialization\Converters\JsonValueConverterDateTimeOffset.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\JsonValueConverterDecimal.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\JsonValueConverterDouble.cs" />
+    <Compile Include="System\Text\Json\Serialization\Converters\JsonConverterEnum.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\JsonValueConverterEnum.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\JsonValueConverterGuid.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\JsonValueConverterInt16.cs" />
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/EnumConverterOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/EnumConverterOptions.cs
new file mode 100644 (file)
index 0000000..9a12bcb
--- /dev/null
@@ -0,0 +1,20 @@
+// 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.
+
+namespace System.Text.Json.Serialization.Converters
+{
+    [Flags]
+    internal enum EnumConverterOptions
+    {
+        /// <summary>
+        /// Allow string values.
+        /// </summary>
+        AllowStrings    = 0b0001,
+
+        /// <summary>
+        /// Allow number values.
+        /// </summary>
+        AllowNumbers    = 0b0010
+    }
+}
@@ -8,13 +8,10 @@ namespace System.Text.Json.Serialization.Converters
 {
     internal sealed class JsonConverterEnum : JsonConverterFactory
     {
-        public JsonConverterEnum(bool treatAsString)
+        public JsonConverterEnum()
         {
-            TreatAsString = treatAsString;
         }
 
-        public bool TreatAsString { get; private set; }
-
         public override bool CanConvert(Type type)
         {
             return type.IsEnum;
@@ -26,7 +23,7 @@ namespace System.Text.Json.Serialization.Converters
                 typeof(JsonConverterEnum<>).MakeGenericType(type),
                 BindingFlags.Instance | BindingFlags.Public,
                 binder: null,
-                new object[] { TreatAsString },
+                new object[] { EnumConverterOptions.AllowNumbers },
                 culture: null);
 
             return converter;
index 6e27812..be23946 100644 (file)
@@ -2,6 +2,8 @@
 // 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.Collections.Concurrent;
+using System.Globalization;
 using System.Runtime.CompilerServices;
 
 namespace System.Text.Json.Serialization.Converters
@@ -9,161 +11,266 @@ namespace System.Text.Json.Serialization.Converters
     internal class JsonConverterEnum<T> : JsonConverter<T>
         where T : struct, Enum
     {
-        private static readonly TypeCode s_enumTypeCode = Type.GetTypeCode(Enum.GetUnderlyingType(typeof(T)));
+        private static readonly TypeCode s_enumTypeCode = Type.GetTypeCode(typeof(T));
 
-        public bool TreatAsString { get; private set; }
+        // 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;
+
+        private readonly EnumConverterOptions _converterOptions;
+        private readonly JsonNamingPolicy _namingPolicy;
+        private readonly ConcurrentDictionary<string, string> _nameCache;
 
         public override bool CanConvert(Type type)
         {
             return type.IsEnum;
         }
 
-        public JsonConverterEnum(bool treatAsString)
+        public JsonConverterEnum(EnumConverterOptions options)
+            : this (options, namingPolicy: null)
         {
-            TreatAsString = treatAsString;
+        }
+
+        public JsonConverterEnum(EnumConverterOptions options, JsonNamingPolicy namingPolicy)
+        {
+            _converterOptions = options;
+            if (namingPolicy != null)
+            {
+                _nameCache = new ConcurrentDictionary<string, string>();
+            }
+            else
+            {
+                namingPolicy = JsonNamingPolicy.Default;
+            }
+            _namingPolicy = namingPolicy;
         }
 
         public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
         {
-            if (TreatAsString)
+            JsonTokenType token = reader.TokenType;
+
+            if (token == JsonTokenType.String)
             {
-                // Assume the token is a string
-                if (reader.TokenType != JsonTokenType.String)
+                if (!_converterOptions.HasFlag(EnumConverterOptions.AllowStrings))
                 {
                     ThrowHelper.ThrowJsonException();
                     return default;
                 }
 
+                // Try parsing case sensitive first
                 string enumString = reader.GetString();
-                if (!Enum.TryParse(enumString, out T value))
+                if (!Enum.TryParse(enumString, out T value)
+                    && !Enum.TryParse(enumString, ignoreCase: true, out value))
                 {
                     ThrowHelper.ThrowJsonException();
                     return default;
                 }
+                return value;
             }
 
-            if (reader.TokenType != JsonTokenType.Number)
+            if (token != JsonTokenType.Number || !_converterOptions.HasFlag(EnumConverterOptions.AllowNumbers))
             {
                 ThrowHelper.ThrowJsonException();
                 return default;
             }
 
-            // When utf8reader/writer will support all primitive types we should remove custom bound checks
-            // https://github.com/dotnet/corefx/issues/36125
-
             switch (s_enumTypeCode)
             {
-                case TypeCode.SByte:
+                // Switch cases ordered by expected frequency
+
+                case TypeCode.Int32:
+                    if (reader.TryGetInt32(out int int32))
                     {
-                        if (reader.TryGetInt32(out int byte8) && JsonHelpers.IsInRangeInclusive(byte8, sbyte.MinValue, sbyte.MaxValue))
-                        {
-                            sbyte byte8Value = (sbyte)byte8;
-                            return Unsafe.As<sbyte, T>(ref byte8Value);
-                        }
-                        break;
+                        return Unsafe.As<int, T>(ref int32);
                     }
-                case TypeCode.Byte:
+                    break;
+                case TypeCode.UInt32:
+                    if (reader.TryGetUInt32(out uint uint32))
                     {
-                        if (reader.TryGetUInt32(out uint ubyte8) && JsonHelpers.IsInRangeInclusive(ubyte8, byte.MinValue, byte.MaxValue))
-                        {
-                            byte ubyte8Value = (byte)ubyte8;
-                            return Unsafe.As<byte, T>(ref ubyte8Value);
-                        }
-                        break;
+                        return Unsafe.As<uint, T>(ref uint32);
                     }
-                case TypeCode.Int16:
+                    break;
+                case TypeCode.UInt64:
+                    if (reader.TryGetUInt64(out ulong uint64))
                     {
-                        if (reader.TryGetInt32(out int int16) && JsonHelpers.IsInRangeInclusive(int16, short.MinValue, short.MaxValue))
-                        {
-                            short shortValue = (short)int16;
-                            return Unsafe.As<short, T>(ref shortValue);
-                        }
-                        break;
+                        return Unsafe.As<ulong, T>(ref uint64);
                     }
-                case TypeCode.UInt16:
+                    break;
+                case TypeCode.Int64:
+                    if (reader.TryGetInt64(out long int64))
                     {
-                        if (reader.TryGetUInt32(out uint uint16) && JsonHelpers.IsInRangeInclusive(uint16, ushort.MinValue, ushort.MaxValue))
-                        {
-                            ushort ushortValue = (ushort)uint16;
-                            return Unsafe.As<ushort, T>(ref ushortValue);
-                        }
-                        break;
+                        return Unsafe.As<long, T>(ref int64);
                     }
-                case TypeCode.Int32:
+                    break;
+
+                // When utf8reader/writer will support all primitive types we should remove custom bound checks
+                // https://github.com/dotnet/corefx/issues/36125
+                case TypeCode.SByte:
+                    if (reader.TryGetInt32(out int byte8) && JsonHelpers.IsInRangeInclusive(byte8, sbyte.MinValue, sbyte.MaxValue))
                     {
-                        if (reader.TryGetInt32(out int int32))
-                        {
-                            return Unsafe.As<int, T>(ref int32);
-                        }
-                        break;
+                        sbyte byte8Value = (sbyte)byte8;
+                        return Unsafe.As<sbyte, T>(ref byte8Value);
                     }
-                case TypeCode.UInt32:
+                    break;
+                case TypeCode.Byte:
+                    if (reader.TryGetUInt32(out uint ubyte8) && JsonHelpers.IsInRangeInclusive(ubyte8, byte.MinValue, byte.MaxValue))
                     {
-                        if (reader.TryGetUInt32(out uint uint32))
-                        {
-                            return Unsafe.As<uint, T>(ref uint32);
-                        }
-                        break;
+                        byte ubyte8Value = (byte)ubyte8;
+                        return Unsafe.As<byte, T>(ref ubyte8Value);
                     }
-                case TypeCode.Int64:
+                    break;
+                case TypeCode.Int16:
+                    if (reader.TryGetInt32(out int int16) && JsonHelpers.IsInRangeInclusive(int16, short.MinValue, short.MaxValue))
                     {
-                        if (reader.TryGetInt64(out long int64))
-                        {
-                            return Unsafe.As<long, T>(ref int64);
-                        }
-                        break;
+                        short shortValue = (short)int16;
+                        return Unsafe.As<short, T>(ref shortValue);
                     }
-                case TypeCode.UInt64:
+                    break;
+                case TypeCode.UInt16:
+                    if (reader.TryGetUInt32(out uint uint16) && JsonHelpers.IsInRangeInclusive(uint16, ushort.MinValue, ushort.MaxValue))
                     {
-                        if (reader.TryGetUInt64(out ulong uint64))
-                        {
-                            return Unsafe.As<ulong, T>(ref uint64);
-                        }
-                        break;
+                        ushort ushortValue = (ushort)uint16;
+                        return Unsafe.As<ushort, T>(ref ushortValue);
                     }
+                    break;
             }
 
             ThrowHelper.ThrowJsonException();
             return default;
         }
 
+        private static bool IsValidIdentifier(string value)
+        {
+            // Trying to do this check efficiently. When an enum is converted to
+            // string the underlying value is given if it can't find a matching
+            // identifier (or identifiers in the case of flags).
+            //
+            // The underlying value will be given back with a digit (e.g. 0-9) possibly
+            // preceded by a negative sign. Identifiers have to start with a letter
+            // so we'll just pick the first valid one and check for a negative sign
+            // if needed.
+            return (value[0] >= 'A' &&
+                (s_negativeSign == null || !value.StartsWith(s_negativeSign)));
+        }
+
         public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
         {
-            if (TreatAsString)
+            // If strings are allowed, attempt to write it out as a string value
+            if (_converterOptions.HasFlag(EnumConverterOptions.AllowStrings))
             {
-                writer.WriteStringValue(value.ToString());
+                string original = value.ToString();
+                if (_nameCache != null && _nameCache.TryGetValue(original, out string transformed))
+                {
+                    writer.WriteStringValue(transformed);
+                    return;
+                }
+
+                if (IsValidIdentifier(original))
+                {
+                    transformed = _namingPolicy.ConvertName(original);
+                    writer.WriteStringValue(transformed);
+                    if (_nameCache != null)
+                    {
+                        _nameCache.TryAdd(original, transformed);
+                    }
+                    return;
+                }
             }
-            else if (s_enumTypeCode == TypeCode.UInt64)
+
+            if (!_converterOptions.HasFlag(EnumConverterOptions.AllowNumbers))
             {
-                // Use the ulong converter to prevent conversion into a signed\long value.
-                ulong ulongValue = Convert.ToUInt64(value);
-                writer.WriteNumberValue(ulongValue);
+                ThrowHelper.ThrowJsonException();
             }
-            else
+
+            switch (s_enumTypeCode)
             {
-                // long can hold the signed\unsigned values of other integer types
-                long longValue = Convert.ToInt64(value);
-                writer.WriteNumberValue(longValue);
+                case TypeCode.Int32:
+                    writer.WriteNumberValue(Unsafe.As<T, int>(ref value));
+                    break;
+                case TypeCode.UInt32:
+                    writer.WriteNumberValue(Unsafe.As<T, uint>(ref value));
+                    break;
+                case TypeCode.UInt64:
+                    writer.WriteNumberValue(Unsafe.As<T, ulong>(ref value));
+                    break;
+                case TypeCode.Int64:
+                    writer.WriteNumberValue(Unsafe.As<T, long>(ref value));
+                    break;
+                case TypeCode.Int16:
+                    writer.WriteNumberValue(Unsafe.As<T, short>(ref value));
+                    break;
+                case TypeCode.UInt16:
+                    writer.WriteNumberValue(Unsafe.As<T, ushort>(ref value));
+                    break;
+                case TypeCode.Byte:
+                    writer.WriteNumberValue(Unsafe.As<T, byte>(ref value));
+                    break;
+                case TypeCode.SByte:
+                    writer.WriteNumberValue(Unsafe.As<T, sbyte>(ref value));
+                    break;
+                default:
+                    ThrowHelper.ThrowJsonException();
+                    break;
             }
         }
 
         public override void Write(Utf8JsonWriter writer, T value, JsonEncodedText propertyName, JsonSerializerOptions options)
         {
-            if (TreatAsString)
+            // If strings are allowed, attempt to write it out as a string value
+            if (_converterOptions.HasFlag(EnumConverterOptions.AllowStrings))
             {
-                writer.WriteString(propertyName, value.ToString());
+                string original = value.ToString();
+                if (_nameCache != null && _nameCache.TryGetValue(original, out string transformed))
+                {
+                    writer.WriteString(propertyName, transformed);
+                    return;
+                }
+
+                if (IsValidIdentifier(original))
+                {
+                    transformed = _namingPolicy.ConvertName(original);
+                    writer.WriteString(propertyName, transformed);
+                    if (_nameCache != null)
+                    {
+                        _nameCache.TryAdd(original, transformed);
+                    }
+                    return;
+                }
             }
-            else if (s_enumTypeCode == TypeCode.UInt64)
+
+            if (!_converterOptions.HasFlag(EnumConverterOptions.AllowNumbers))
             {
-                // Use the ulong converter to prevent conversion into a signed\long value.
-                ulong ulongValue = Convert.ToUInt64(value);
-                writer.WriteNumber(propertyName, ulongValue);
+                ThrowHelper.ThrowJsonException();
             }
-            else
+
+            switch (s_enumTypeCode)
             {
-                // long can hold the signed\unsigned values of other integer types.
-                long longValue = Convert.ToInt64(value);
-                writer.WriteNumber(propertyName, longValue);
+                case TypeCode.Int32:
+                    writer.WriteNumber(propertyName, Unsafe.As<T, int>(ref value));
+                    break;
+                case TypeCode.UInt32:
+                    writer.WriteNumber(propertyName, Unsafe.As<T, uint>(ref value));
+                    break;
+                case TypeCode.UInt64:
+                    writer.WriteNumber(propertyName, Unsafe.As<T, ulong>(ref value));
+                    break;
+                case TypeCode.Int64:
+                    writer.WriteNumber(propertyName, Unsafe.As<T, long>(ref value));
+                    break;
+                case TypeCode.Int16:
+                    writer.WriteNumber(propertyName, Unsafe.As<T, short>(ref value));
+                    break;
+                case TypeCode.UInt16:
+                    writer.WriteNumber(propertyName, Unsafe.As<T, ushort>(ref value));
+                    break;
+                case TypeCode.Byte:
+                    writer.WriteNumber(propertyName, Unsafe.As<T, byte>(ref value));
+                    break;
+                case TypeCode.SByte:
+                    writer.WriteNumber(propertyName, Unsafe.As<T, sbyte>(ref value));
+                    break;
+                default:
+                    ThrowHelper.ThrowJsonException();
+                    break;
             }
         }
     }
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonDefaultNamingPolicy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonDefaultNamingPolicy.cs
new file mode 100644 (file)
index 0000000..0f56d40
--- /dev/null
@@ -0,0 +1,11 @@
+// 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.
+
+namespace System.Text.Json
+{
+    internal class JsonDefaultNamingPolicy : JsonNamingPolicy
+    {
+        public override string ConvertName(string name) => name;
+    }
+}
index 1c8eeda..387d7ba 100644 (file)
@@ -19,6 +19,8 @@ namespace System.Text.Json
         /// </summary>
         public static JsonNamingPolicy CamelCase { get; } = new JsonCamelCaseNamePolicy();
 
+        internal static JsonNamingPolicy Default { get; } = new JsonDefaultNamingPolicy();
+
         /// <summary>
         /// When overridden in a derived class, converts the specified name according to the policy.
         /// </summary>
index bb14f5f..4776b11 100644 (file)
@@ -47,7 +47,7 @@ namespace System.Text.Json
             var converters = new List<JsonConverter>(NumberOfConverters);
 
             // Use a list for converters that implement CanConvert().
-            converters.Add(new JsonConverterEnum(treatAsString: false));
+            converters.Add(new JsonConverterEnum());
             converters.Add(new JsonKeyValuePairConverter());
 
             // We will likely add collection converters here in the future.
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonStringEnumConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonStringEnumConverter.cs
new file mode 100644 (file)
index 0000000..f59195d
--- /dev/null
@@ -0,0 +1,58 @@
+// 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.Reflection;
+using System.Text.Json.Serialization.Converters;
+
+namespace System.Text.Json.Serialization
+{
+    /// <summary>
+    /// Converter to convert enums to and from strings.
+    /// </summary>
+    /// <remarks>
+    /// Reading is case insensitive, writing can be customized via a <see cref="JsonNamingPolicy" />.
+    /// </remarks>
+    public sealed class JsonStringEnumConverter : JsonConverterFactory
+    {
+        private readonly JsonNamingPolicy _namingPolicy;
+        private readonly EnumConverterOptions _converterOptions;
+
+        /// <summary>
+        /// Constructor.
+        /// </summary>
+        /// <param name="namingPolicy">
+        /// Optional naming policy for writing enum values.
+        /// </param>
+        /// <param name="allowIntegerValues">
+        /// True to allow undefined enum values. When true, if an enum value isn't
+        /// defined it will output as a number rather than a string.
+        /// </param>
+        public JsonStringEnumConverter(JsonNamingPolicy namingPolicy = null, bool allowIntegerValues = true)
+        {
+            _namingPolicy = namingPolicy;
+            _converterOptions = allowIntegerValues
+                ? EnumConverterOptions.AllowNumbers | EnumConverterOptions.AllowStrings
+                : EnumConverterOptions.AllowStrings;
+        }
+
+        /// <inheritdoc />
+        public override bool CanConvert(Type type)
+        {
+            return type.IsEnum;
+        }
+
+        /// <inheritdoc />
+        protected override JsonConverter CreateConverter(Type type)
+        {
+            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
+                typeof(JsonConverterEnum<>).MakeGenericType(type),
+                BindingFlags.Instance | BindingFlags.Public,
+                binder: null,
+                new object[] { _converterOptions, _namingPolicy },
+                culture: null);
+
+            return converter;
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/tests/Serialization/EnumConverterTests.cs b/src/libraries/System.Text.Json/tests/Serialization/EnumConverterTests.cs
new file mode 100644 (file)
index 0000000..e6171c6
--- /dev/null
@@ -0,0 +1,117 @@
+// 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.IO;
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+    public class EnumConverterTests
+    {
+        [Fact]
+        public void ConvertDayOfWeek()
+        {
+            JsonSerializerOptions options = new JsonSerializerOptions();
+            options.Converters.Add(new JsonStringEnumConverter());
+
+            WhenClass when = JsonSerializer.Parse<WhenClass>(@"{""Day"":""Monday""}", options);
+            Assert.Equal(DayOfWeek.Monday, when.Day);
+            DayOfWeek day = JsonSerializer.Parse<DayOfWeek>(@"""Tuesday""", options);
+            Assert.Equal(DayOfWeek.Tuesday, day);
+
+            // We are case insensitive on read
+            day = JsonSerializer.Parse<DayOfWeek>(@"""wednesday""", options);
+            Assert.Equal(DayOfWeek.Wednesday, day);
+
+            // Numbers work by default
+            day = JsonSerializer.Parse<DayOfWeek>(@"4", options);
+            Assert.Equal(DayOfWeek.Thursday, day);
+
+            string json = JsonSerializer.ToString(DayOfWeek.Friday, options);
+            Assert.Equal(@"""Friday""", json);
+
+            // Try a unique naming policy
+            options = new JsonSerializerOptions();
+            options.Converters.Add(new JsonStringEnumConverter(new ToLower()));
+
+            json = JsonSerializer.ToString(DayOfWeek.Friday, options);
+            Assert.Equal(@"""friday""", json);
+
+            // Undefined values should come out as a number (not a string)
+            json = JsonSerializer.ToString((DayOfWeek)(-1), options);
+            Assert.Equal(@"-1", json);
+
+            // Not permitting integers should throw
+            options = new JsonSerializerOptions();
+            options.Converters.Add(new JsonStringEnumConverter(allowIntegerValues: false));
+            Assert.Throws<JsonException>(() => JsonSerializer.ToString((DayOfWeek)(-1), options));
+        }
+
+        public class ToLower : JsonNamingPolicy
+        {
+            public override string ConvertName(string name) => name.ToLowerInvariant();
+        }
+
+        public class WhenClass
+        {
+            public DayOfWeek Day { get; set; }
+        }
+
+        [Fact]
+        public void ConvertFileAttributes()
+        {
+            JsonSerializerOptions options = new JsonSerializerOptions();
+            options.Converters.Add(new JsonStringEnumConverter());
+
+            FileState state = JsonSerializer.Parse<FileState>(@"{""Attributes"":""ReadOnly""}", options);
+            Assert.Equal(FileAttributes.ReadOnly, state.Attributes);
+            state = JsonSerializer.Parse<FileState>(@"{""Attributes"":""Directory, ReparsePoint""}", options);
+            Assert.Equal(FileAttributes.Directory | FileAttributes.ReparsePoint, state.Attributes);
+            FileAttributes attributes = JsonSerializer.Parse<FileAttributes>(@"""Normal""", options);
+            Assert.Equal(FileAttributes.Normal, attributes);
+            attributes = JsonSerializer.Parse<FileAttributes>(@"""System, SparseFile""", options);
+            Assert.Equal(FileAttributes.System | FileAttributes.SparseFile, attributes);
+
+            // We are case insensitive on read
+            attributes = JsonSerializer.Parse<FileAttributes>(@"""OFFLINE""", options);
+            Assert.Equal(FileAttributes.Offline, attributes);
+            attributes = JsonSerializer.Parse<FileAttributes>(@"""compressed, notcontentindexed""", options);
+            Assert.Equal(FileAttributes.Compressed | FileAttributes.NotContentIndexed, attributes);
+
+            // Numbers are cool by default
+            attributes = JsonSerializer.Parse<FileAttributes>(@"131072", options);
+            Assert.Equal(FileAttributes.NoScrubData, attributes);
+            attributes = JsonSerializer.Parse<FileAttributes>(@"3", options);
+            Assert.Equal(FileAttributes.Hidden | FileAttributes.ReadOnly, attributes);
+
+            string json = JsonSerializer.ToString(FileAttributes.Hidden, options);
+            Assert.Equal(@"""Hidden""", json);
+            json = JsonSerializer.ToString(FileAttributes.Temporary | FileAttributes.Offline, options);
+            Assert.Equal(@"""Temporary, Offline""", json);
+
+            // Try a unique casing
+            options = new JsonSerializerOptions();
+            options.Converters.Add(new JsonStringEnumConverter(new ToLower()));
+
+            json = JsonSerializer.ToString(FileAttributes.NoScrubData, options);
+            Assert.Equal(@"""noscrubdata""", json);
+            json = JsonSerializer.ToString(FileAttributes.System | FileAttributes.Offline, options);
+            Assert.Equal(@"""system, offline""", json);
+
+            // Undefined values should come out as a number (not a string)
+            json = JsonSerializer.ToString((FileAttributes)(-1), options);
+            Assert.Equal(@"-1", json);
+
+            // Not permitting integers should throw
+            options = new JsonSerializerOptions();
+            options.Converters.Add(new JsonStringEnumConverter(allowIntegerValues: false));
+            Assert.Throws<JsonException>(() => JsonSerializer.ToString((FileAttributes)(-1), options));
+        }
+
+        public class FileState
+        {
+            public FileAttributes Attributes { get; set; }
+        }
+    }
+}
index d624a00..8d9d141 100644 (file)
@@ -50,6 +50,7 @@
     <Compile Include="Serialization\CustomConverterTests.Callback.cs" />
     <Compile Include="Serialization\CyclicTests.cs" />
     <Compile Include="Serialization\DictionaryTests.cs" />
+    <Compile Include="Serialization\EnumConverterTests.cs" />
     <Compile Include="Serialization\EnumTests.cs" />
     <Compile Include="Serialization\ExceptionTests.cs" />
     <Compile Include="Serialization\ExtensionDataTests.cs" />