diff --git a/Src/Newtonsoft.Json.Tests/Converters/StringEnumConverterTests.cs b/Src/Newtonsoft.Json.Tests/Converters/StringEnumConverterTests.cs index 1b333322..2d5c8f53 100644 --- a/Src/Newtonsoft.Json.Tests/Converters/StringEnumConverterTests.cs +++ b/Src/Newtonsoft.Json.Tests/Converters/StringEnumConverterTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; using System.Text; using Newtonsoft.Json.Converters; using NUnit.Framework; @@ -17,6 +18,11 @@ namespace Newtonsoft.Json.Tests.Converters public StoreColor? NullableStoreColor2 { get; set; } } + public class EnumContainer + { + public T Enum { get; set; } + } + public enum NegativeEnum { Negative = -1, @@ -24,12 +30,88 @@ namespace Newtonsoft.Json.Tests.Converters Positive = 1 } +#if !NET20 + public enum NamedEnum + { + [EnumMember(Value = "@first")] + First, + [EnumMember(Value = "@second")] + Second, + Third + } + + public enum NamedEnumDuplicate + { + [EnumMember(Value = "Third")] + First, + [EnumMember(Value = "@second")] + Second, + Third + } +#endif + public class NegativeEnumClass { public NegativeEnum Value1 { get; set; } public NegativeEnum Value2 { get; set; } } +#if !NET20 + [Test] + [ExpectedException(typeof(Exception), ExpectedMessage = "Enum name 'Third' already exists on enum 'NamedEnumDuplicate'.")] + public void NamedEnumDuplicateTest() + { + EnumContainer c = new EnumContainer + { + Enum = NamedEnumDuplicate.First + }; + + JsonConvert.SerializeObject(c, Formatting.Indented, new StringEnumConverter()); + } + + [Test] + public void SerializeNameEnumTest() + { + EnumContainer c = new EnumContainer + { + Enum = NamedEnum.First + }; + + string json = JsonConvert.SerializeObject(c, Formatting.Indented, new StringEnumConverter()); + Assert.AreEqual(@"{ + ""Enum"": ""@first"" +}", json); + + c = new EnumContainer + { + Enum = NamedEnum.Third + }; + + json = JsonConvert.SerializeObject(c, Formatting.Indented, new StringEnumConverter()); + Assert.AreEqual(@"{ + ""Enum"": ""Third"" +}", json); + } + + [Test] + public void DeserializeNameEnumTest() + { + string json = @"{ + ""Enum"": ""@first"" +}"; + + EnumContainer c = JsonConvert.DeserializeObject>(json, new StringEnumConverter()); + Assert.AreEqual(NamedEnum.First, c.Enum); + + json = @"{ + ""Enum"": ""Third"" +}"; + + c = JsonConvert.DeserializeObject>(json, new StringEnumConverter()); + Assert.AreEqual(NamedEnum.Third, c.Enum); + } +#endif + [Test] public void SerializeEnumClass() { diff --git a/Src/Newtonsoft.Json/Converters/StringEnumConverter.cs b/Src/Newtonsoft.Json/Converters/StringEnumConverter.cs index 83e7b901..16087562 100644 --- a/Src/Newtonsoft.Json/Converters/StringEnumConverter.cs +++ b/Src/Newtonsoft.Json/Converters/StringEnumConverter.cs @@ -24,22 +24,31 @@ #endregion using System; +using System.Collections.Generic; using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; using Newtonsoft.Json.Utilities; namespace Newtonsoft.Json.Converters { + /// + /// Converts an to and from its name string value. + /// /// /// Converts an to and from its name string value. /// public class StringEnumConverter : JsonConverter { + private readonly Dictionary> _enumMemberNamesPerType = new Dictionary>(); + /// /// Gets or sets a value indicating whether the written enum text should be camel case. /// /// true if the written enum text will be camel case; otherwise, false. public bool CamelCaseText { get; set; } - + /// /// Writes the JSON representation of the object. /// @@ -54,21 +63,26 @@ namespace Newtonsoft.Json.Converters return; } - Enum e = (Enum) value; + Enum e = (Enum)value; + string enumName = e.ToString("G"); if (char.IsNumber(enumName[0]) || enumName[0] == '-') { - // enum value didn't have an option against it - // fallback to writing number value writer.WriteValue(value); } else { - if (CamelCaseText) - enumName = StringUtils.ToCamelCase(enumName); + BidirectionalDictionary map = GetEnumNameMap(e.GetType()); - writer.WriteValue(enumName); + string resolvedEnumName; + map.TryGetByFirst(enumName, out resolvedEnumName); + resolvedEnumName = resolvedEnumName ?? enumName; + + if (CamelCaseText) + resolvedEnumName = StringUtils.ToCamelCase(resolvedEnumName); + + writer.WriteValue(resolvedEnumName); } } @@ -83,8 +97,8 @@ namespace Newtonsoft.Json.Converters public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { Type t = (ReflectionUtils.IsNullableType(objectType)) - ? Nullable.GetUnderlyingType(objectType) - : objectType; + ? Nullable.GetUnderlyingType(objectType) + : objectType; if (reader.TokenType == JsonToken.Null) { @@ -95,26 +109,84 @@ namespace Newtonsoft.Json.Converters } if (reader.TokenType == JsonToken.String) - return Enum.Parse(t, reader.Value.ToString(), true); - + { + var map = GetEnumNameMap(t); + string resolvedEnumName; + map.TryGetBySecond(reader.Value.ToString(), out resolvedEnumName); + resolvedEnumName = resolvedEnumName ?? reader.Value.ToString(); + + return Enum.Parse(t, resolvedEnumName, true); + } + if (reader.TokenType == JsonToken.Integer) return ConvertUtils.ConvertOrCast(reader.Value, CultureInfo.InvariantCulture, t); - + throw new Exception("Unexpected token when parsing enum. Expected String or Integer, got {0}.".FormatWith(CultureInfo.InvariantCulture, reader.TokenType)); } + /// + /// A cached representation of the Enum string representation to respect per Enum field name. + /// + /// The type of the Enum. + /// A map of enum field name to either the field name, or the configured enum member name (). + private BidirectionalDictionary GetEnumNameMap(Type t) + { + BidirectionalDictionary map; + + if (!_enumMemberNamesPerType.TryGetValue(t, out map)) + { + lock (_enumMemberNamesPerType) + { + if (_enumMemberNamesPerType.TryGetValue(t, out map)) + return map; + + map = new BidirectionalDictionary( + StringComparer.OrdinalIgnoreCase, + StringComparer.OrdinalIgnoreCase); + + foreach (FieldInfo f in t.GetFields()) + { + string n1 = f.Name; + string n2; + +#if !NET20 + n2 = f.GetCustomAttributes(typeof (EnumMemberAttribute), true) + .Cast() + .Select(a => a.Value) + .SingleOrDefault() ?? f.Name; +#else + n2 = f.Name; +#endif + + string s; + if (map.TryGetBySecond(n2, out s)) + { + throw new Exception("Enum name '{0}' already exists on enum '{1}'." + .FormatWith(CultureInfo.InvariantCulture, n2, t.Name)); + } + + map.Add(n1, n2); + } + + _enumMemberNamesPerType[t] = map; + } + } + + return map; + } + /// /// Determines whether this instance can convert the specified object type. /// /// Type of the object. /// - /// true if this instance can convert the specified object type; otherwise, false. + /// true if this instance can convert the specified object type; otherwise, false. /// public override bool CanConvert(Type objectType) { Type t = (ReflectionUtils.IsNullableType(objectType)) - ? Nullable.GetUnderlyingType(objectType) - : objectType; + ? Nullable.GetUnderlyingType(objectType) + : objectType; return t.IsEnum; } diff --git a/Src/Newtonsoft.Json/Linq/JValue.cs b/Src/Newtonsoft.Json/Linq/JValue.cs index 4b05a58f..6bd01a96 100644 --- a/Src/Newtonsoft.Json/Linq/JValue.cs +++ b/Src/Newtonsoft.Json/Linq/JValue.cs @@ -177,12 +177,15 @@ namespace Newtonsoft.Json.Linq return b1.CompareTo(b2); case JTokenType.Date: +#if !NET20 if (objA is DateTime) { +#endif DateTime date1 = Convert.ToDateTime(objA, CultureInfo.InvariantCulture); DateTime date2 = Convert.ToDateTime(objB, CultureInfo.InvariantCulture); return date1.CompareTo(date2); +#if !NET20 } else { @@ -194,6 +197,7 @@ namespace Newtonsoft.Json.Linq return date1.CompareTo(date2); } +#endif case JTokenType.Bytes: if (!(objB is byte[])) throw new ArgumentException("Object must be of type byte[].");