From 3674af91193c263e8994919a2d00a48cd02e4480 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 20 Mar 2016 19:13:44 +1300 Subject: [PATCH 1/6] -Added INamingStrategy -Added CamelCaseNamingStrategy -Added NamingStrategyType and NamingStrategyParameters to JsonPropertyAttribute -Added NamingStrategyType and NamingStrategyParameters to JsonContainerAttribute --- .../Newtonsoft.Json.Tests.Net20.csproj | 1 + .../Newtonsoft.Json.Tests.Net35.csproj | 1 + .../Newtonsoft.Json.Tests.Net40.csproj | 1 + .../Newtonsoft.Json.Tests.Portable.csproj | 1 + .../Newtonsoft.Json.Tests.Portable40.csproj | 1 + .../Newtonsoft.Json.Tests.csproj | 1 + .../CamelCaseNamingStrategyTests.cs | 327 ++++++++++++++++++ Src/Newtonsoft.Json/JsonContainerAttribute.cs | 25 ++ Src/Newtonsoft.Json/JsonPropertyAttribute.cs | 3 + .../Newtonsoft.Json.Net20.csproj | 3 + .../Newtonsoft.Json.Net35.csproj | 3 + .../Newtonsoft.Json.Net40.csproj | 3 + .../Newtonsoft.Json.Portable.csproj | 3 + .../Newtonsoft.Json.Portable40.csproj | 3 + Src/Newtonsoft.Json/Newtonsoft.Json.csproj | 3 + .../Serialization/CamelCaseNamingStrategy.cs | 40 +++ .../CamelCasePropertyNamesContractResolver.cs | 16 +- .../Serialization/DefaultContractResolver.cs | 68 +++- .../Serialization/DefaultNamingStrategy.cs | 15 + .../Serialization/INamingStrategy.cs | 8 + .../Serialization/JsonTypeReflector.cs | 60 ++-- 21 files changed, 549 insertions(+), 37 deletions(-) create mode 100644 Src/Newtonsoft.Json.Tests/Serialization/CamelCaseNamingStrategyTests.cs create mode 100644 Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs create mode 100644 Src/Newtonsoft.Json/Serialization/DefaultNamingStrategy.cs create mode 100644 Src/Newtonsoft.Json/Serialization/INamingStrategy.cs diff --git a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net20.csproj b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net20.csproj index 5dc30cae..648bf706 100644 --- a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net20.csproj +++ b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net20.csproj @@ -96,6 +96,7 @@ + diff --git a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net35.csproj b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net35.csproj index 1427dc21..d5fa425b 100644 --- a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net35.csproj +++ b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net35.csproj @@ -139,6 +139,7 @@ + diff --git a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net40.csproj b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net40.csproj index 7c6e598b..f0cdaacf 100644 --- a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net40.csproj +++ b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net40.csproj @@ -257,6 +257,7 @@ + diff --git a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable.csproj b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable.csproj index 0e7c701f..b39e015c 100644 --- a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable.csproj +++ b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable.csproj @@ -132,6 +132,7 @@ + diff --git a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable40.csproj b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable40.csproj index 4e3230e8..0b84de7d 100644 --- a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable40.csproj +++ b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable40.csproj @@ -136,6 +136,7 @@ + diff --git a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj index 4d0de2a9..0d74f34c 100644 --- a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj +++ b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj @@ -262,6 +262,7 @@ + diff --git a/Src/Newtonsoft.Json.Tests/Serialization/CamelCaseNamingStrategyTests.cs b/Src/Newtonsoft.Json.Tests/Serialization/CamelCaseNamingStrategyTests.cs new file mode 100644 index 00000000..392933ea --- /dev/null +++ b/Src/Newtonsoft.Json.Tests/Serialization/CamelCaseNamingStrategyTests.cs @@ -0,0 +1,327 @@ +#region License +// Copyright (c) 2007 James Newton-King +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +#endregion + +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Serialization; +#if NETFX_CORE +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using TestFixture = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestClassAttribute; +using Test = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestMethodAttribute; +#elif DNXCORE50 +using Xunit; +using Test = Xunit.FactAttribute; +using Assert = Newtonsoft.Json.Tests.XUnitAssert; +#else +using NUnit.Framework; +#endif +using Newtonsoft.Json.Tests.TestObjects; +using Newtonsoft.Json.Linq; +using System.Reflection; +using Newtonsoft.Json.Utilities; + +namespace Newtonsoft.Json.Tests.Serialization +{ + [TestFixture] + public class CamelCaseNamingStrategyTests : TestFixtureBase + { + [Test] + public void JsonConvertSerializerSettings() + { + Person person = new Person(); + person.BirthDate = new DateTime(2000, 11, 20, 23, 55, 44, DateTimeKind.Utc); + person.LastModified = new DateTime(2000, 11, 20, 23, 55, 44, DateTimeKind.Utc); + person.Name = "Name!"; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy() + }; + + string json = JsonConvert.SerializeObject(person, Formatting.Indented, new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + StringAssert.AreEqual(@"{ + ""name"": ""Name!"", + ""birthDate"": ""2000-11-20T23:55:44Z"", + ""lastModified"": ""2000-11-20T23:55:44Z"" +}", json); + + Person deserializedPerson = JsonConvert.DeserializeObject(json, new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + Assert.AreEqual(person.BirthDate, deserializedPerson.BirthDate); + Assert.AreEqual(person.LastModified, deserializedPerson.LastModified); + Assert.AreEqual(person.Name, deserializedPerson.Name); + + json = JsonConvert.SerializeObject(person, Formatting.Indented); + StringAssert.AreEqual(@"{ + ""Name"": ""Name!"", + ""BirthDate"": ""2000-11-20T23:55:44Z"", + ""LastModified"": ""2000-11-20T23:55:44Z"" +}", json); + } + + [Test] + public void JTokenWriter_OverrideSpecifiedName() + { + JsonIgnoreAttributeOnClassTestClass ignoreAttributeOnClassTestClass = new JsonIgnoreAttributeOnClassTestClass(); + ignoreAttributeOnClassTestClass.Field = int.MinValue; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy + { + OverrideSpecifiedNames = true + } + }; + + JsonSerializer serializer = new JsonSerializer(); + serializer.ContractResolver = contractResolver; + + JTokenWriter writer = new JTokenWriter(); + + serializer.Serialize(writer, ignoreAttributeOnClassTestClass); + + JObject o = (JObject)writer.Token; + JProperty p = o.Property("theField"); + + Assert.IsNotNull(p); + Assert.AreEqual(int.MinValue, (int)p.Value); + } + + [Test] + public void BlogPostExample() + { + Product product = new Product + { + ExpiryDate = new DateTime(2010, 12, 20, 18, 1, 0, DateTimeKind.Utc), + Name = "Widget", + Price = 9.99m, + Sizes = new[] { "Small", "Medium", "Large" } + }; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy() + }; + + string json = + JsonConvert.SerializeObject( + product, + Formatting.Indented, + new JsonSerializerSettings { ContractResolver = contractResolver } + ); + + //{ + // "name": "Widget", + // "expiryDate": "\/Date(1292868060000)\/", + // "price": 9.99, + // "sizes": [ + // "Small", + // "Medium", + // "Large" + // ] + //} + + StringAssert.AreEqual(@"{ + ""name"": ""Widget"", + ""expiryDate"": ""2010-12-20T18:01:00Z"", + ""price"": 9.99, + ""sizes"": [ + ""Small"", + ""Medium"", + ""Large"" + ] +}", json); + } + +#if !(NET35 || NET20 || PORTABLE40) + [Test] + public void DynamicCamelCasePropertyNames() + { + dynamic o = new TestDynamicObject(); + o.Text = "Text!"; + o.Integer = int.MaxValue; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy + { + CamelCaseDictionaryKeys = true + } + }; + + string json = JsonConvert.SerializeObject(o, Formatting.Indented, + new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + StringAssert.AreEqual(@"{ + ""explicit"": false, + ""text"": ""Text!"", + ""integer"": 2147483647, + ""int"": 0, + ""childObject"": null +}", json); + } +#endif + + [Test] + public void DictionaryCamelCasePropertyNames_Disabled() + { + Dictionary values = new Dictionary + { + { "First", "Value1!" }, + { "Second", "Value2!" } + }; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy() + }; + + string json = JsonConvert.SerializeObject(values, Formatting.Indented, + new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + StringAssert.AreEqual(@"{ + ""First"": ""Value1!"", + ""Second"": ""Value2!"" +}", json); + } + + [Test] + public void DictionaryCamelCasePropertyNames_Enabled() + { + Dictionary values = new Dictionary + { + { "First", "Value1!" }, + { "Second", "Value2!" } + }; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy + { + CamelCaseDictionaryKeys = true + } + }; + + string json = JsonConvert.SerializeObject(values, Formatting.Indented, + new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + StringAssert.AreEqual(@"{ + ""first"": ""Value1!"", + ""second"": ""Value2!"" +}", json); + } + + public class PropertyAttributeNamingStrategyTestClass + { + [JsonProperty] + public string HasNoAttributeNamingStrategy { get; set; } + + [JsonProperty(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public string HasAttributeNamingStrategy { get; set; } + } + + [Test] + public void JsonPropertyAttribute_NamingStrategyType() + { + PropertyAttributeNamingStrategyTestClass c = new PropertyAttributeNamingStrategyTestClass + { + HasNoAttributeNamingStrategy = "Value1!", + HasAttributeNamingStrategy = "Value2!" + }; + + string json = JsonConvert.SerializeObject(c, Formatting.Indented); + + StringAssert.AreEqual(@"{ + ""HasNoAttributeNamingStrategy"": ""Value1!"", + ""hasAttributeNamingStrategy"": ""Value2!"" +}", json); + } + + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] + public class ContainerAttributeNamingStrategyTestClass + { + public string Prop1 { get; set; } + public string Prop2 { get; set; } + [JsonProperty(NamingStrategyType = typeof(DefaultNamingStrategy))] + public string HasAttributeNamingStrategy { get; set; } + } + + [Test] + public void JsonObjectAttribute_NamingStrategyType() + { + ContainerAttributeNamingStrategyTestClass c = new ContainerAttributeNamingStrategyTestClass + { + Prop1 = "Value1!", + Prop2 = "Value2!" + }; + + string json = JsonConvert.SerializeObject(c, Formatting.Indented); + + StringAssert.AreEqual(@"{ + ""prop1"": ""Value1!"", + ""prop2"": ""Value2!"", + ""HasAttributeNamingStrategy"": null +}", json); + } + + [JsonDictionary(NamingStrategyType = typeof(CamelCaseNamingStrategy), NamingStrategyParameters = new object[] { true, true })] + public class DictionaryAttributeNamingStrategyTestClass : Dictionary + { + } + + [Test] + public void JsonDictionaryAttribute_NamingStrategyType() + { + DictionaryAttributeNamingStrategyTestClass c = new DictionaryAttributeNamingStrategyTestClass + { + ["Key1"] = "Value1!", + ["Key2"] = "Value2!" + }; + + string json = JsonConvert.SerializeObject(c, Formatting.Indented); + + StringAssert.AreEqual(@"{ + ""key1"": ""Value1!"", + ""key2"": ""Value2!"" +}", json); + } + } +} \ No newline at end of file diff --git a/Src/Newtonsoft.Json/JsonContainerAttribute.cs b/Src/Newtonsoft.Json/JsonContainerAttribute.cs index cfc1b58f..bf894657 100644 --- a/Src/Newtonsoft.Json/JsonContainerAttribute.cs +++ b/Src/Newtonsoft.Json/JsonContainerAttribute.cs @@ -24,6 +24,7 @@ #endregion using System; +using Newtonsoft.Json.Serialization; namespace Newtonsoft.Json { @@ -68,12 +69,36 @@ namespace Newtonsoft.Json /// public object[] ItemConverterParameters { get; set; } + public Type NamingStrategyType + { + get { return _namingStrategyType; } + set + { + _namingStrategyType = value; + NamingStrategy = null; + } + } + + public object[] NamingStrategyParameters + { + get { return _namingStrategyParameters; } + set + { + _namingStrategyParameters = value; + NamingStrategy = null; + } + } + + internal INamingStrategy NamingStrategy { get; set; } + // yuck. can't set nullable properties on an attribute in C# // have to use this approach to get an unset default state internal bool? _isReference; internal bool? _itemIsReference; internal ReferenceLoopHandling? _itemReferenceLoopHandling; internal TypeNameHandling? _itemTypeNameHandling; + private Type _namingStrategyType; + private object[] _namingStrategyParameters; /// /// Gets or sets a value that indicates whether to preserve object references. diff --git a/Src/Newtonsoft.Json/JsonPropertyAttribute.cs b/Src/Newtonsoft.Json/JsonPropertyAttribute.cs index 6efe0ed7..211bd842 100644 --- a/Src/Newtonsoft.Json/JsonPropertyAttribute.cs +++ b/Src/Newtonsoft.Json/JsonPropertyAttribute.cs @@ -64,6 +64,9 @@ namespace Newtonsoft.Json /// public object[] ItemConverterParameters { get; set; } + public Type NamingStrategyType { get; set; } + public object[] NamingStrategyParameters { get; set; } + /// /// Gets or sets the null value handling used when serializing this property. /// diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.Net20.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.Net20.csproj index b63455d0..768041e0 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.Net20.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.Net20.csproj @@ -128,9 +128,12 @@ + + + diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.Net35.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.Net35.csproj index 09ea61c9..1330dc52 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.Net35.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.Net35.csproj @@ -142,9 +142,12 @@ + + + diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.Net40.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.Net40.csproj index e4cf0e32..f78c0ef7 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.Net40.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.Net40.csproj @@ -141,9 +141,12 @@ + + + diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.Portable.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.Portable.csproj index 2ff19dd6..963f2d2f 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.Portable.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.Portable.csproj @@ -164,7 +164,9 @@ + + @@ -175,6 +177,7 @@ + diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.Portable40.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.Portable40.csproj index 762db4d0..9ff94ac4 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.Portable40.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.Portable40.csproj @@ -169,8 +169,10 @@ + + @@ -180,6 +182,7 @@ + diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.csproj index d69978da..363c7dda 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.csproj @@ -168,8 +168,10 @@ + + @@ -179,6 +181,7 @@ + diff --git a/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs new file mode 100644 index 00000000..a2d8a8c8 --- /dev/null +++ b/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json.Utilities; + +namespace Newtonsoft.Json.Serialization +{ + public class CamelCaseNamingStrategy : INamingStrategy + { + public bool CamelCaseDictionaryKeys { get; set; } + public bool OverrideSpecifiedNames { get; set; } + + public CamelCaseNamingStrategy() + { + } + + public CamelCaseNamingStrategy(bool camelCaseDictionaryKeys, bool overrideSpecifiedNames) + { + CamelCaseDictionaryKeys = camelCaseDictionaryKeys; + OverrideSpecifiedNames = overrideSpecifiedNames; + } + + public string GetPropertyName(string s, bool hasSpecifiedName) + { + if (hasSpecifiedName && !OverrideSpecifiedNames) + { + return s; + } + + return StringUtils.ToCamelCase(s); + } + + public string GetDictionaryKey(string s) + { + if (!CamelCaseDictionaryKeys) + { + return s; + } + + return StringUtils.ToCamelCase(s); + } + } +} \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Serialization/CamelCasePropertyNamesContractResolver.cs b/Src/Newtonsoft.Json/Serialization/CamelCasePropertyNamesContractResolver.cs index ea9ee82d..42061260 100644 --- a/Src/Newtonsoft.Json/Serialization/CamelCasePropertyNamesContractResolver.cs +++ b/Src/Newtonsoft.Json/Serialization/CamelCasePropertyNamesContractResolver.cs @@ -41,17 +41,11 @@ namespace Newtonsoft.Json.Serialization : base(true) #pragma warning restore 612,618 { - } - - /// - /// Resolves the name of the property. - /// - /// Name of the property. - /// The property name camel cased. - protected override string ResolvePropertyName(string propertyName) - { - // lower case the first letter (or more) of the passed in name - return StringUtils.ToCamelCase(propertyName); + NamingStrategy = new CamelCaseNamingStrategy + { + CamelCaseDictionaryKeys = true, + OverrideSpecifiedNames = true + }; } } } \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs b/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs index bf970338..3c0b769e 100644 --- a/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs +++ b/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs @@ -185,6 +185,8 @@ namespace Newtonsoft.Json.Serialization public bool IgnoreSerializableAttribute { get; set; } #endif + public INamingStrategy NamingStrategy { get; set; } + /// /// Initializes a new instance of the class. /// @@ -738,7 +740,6 @@ namespace Newtonsoft.Json.Serialization #if NET35 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Portability", "CA1903:UseOnlyApiFromTargetedFramework", MessageId = "System.Runtime.Serialization.DataContractAttribute.#get_IsReference()")] #endif - private void InitializeContract(JsonContract contract) { JsonContainerAttribute containerAttribute = JsonTypeReflector.GetCachedAttribute(contract.NonNullableUnderlyingType); @@ -906,7 +907,9 @@ namespace Newtonsoft.Json.Serialization #endif #if DOTNET if (t.IsGenericType() && t.GetGenericTypeDefinition() == typeof(ConcurrentDictionary<,>)) + { return true; + } #endif return false; @@ -938,7 +941,16 @@ namespace Newtonsoft.Json.Serialization JsonDictionaryContract contract = new JsonDictionaryContract(objectType); InitializeContract(contract); - contract.DictionaryKeyResolver = ResolveDictionaryKey; + JsonContainerAttribute containerAttribute = JsonTypeReflector.GetAttribute(objectType); + if (containerAttribute?.NamingStrategyType != null) + { + INamingStrategy namingStrategy = JsonTypeReflector.GetContainerNamingStrategy(containerAttribute); + contract.DictionaryKeyResolver = s => namingStrategy.GetDictionaryKey(s); + } + else + { + contract.DictionaryKeyResolver = ResolveDictionaryKey; + } ConstructorInfo overrideConstructor = GetAttributeConstructor(contract.NonNullableUnderlyingType); @@ -1066,7 +1078,17 @@ namespace Newtonsoft.Json.Serialization JsonDynamicContract contract = new JsonDynamicContract(objectType); InitializeContract(contract); - contract.PropertyNameResolver = ResolvePropertyName; + JsonContainerAttribute containerAttribute = JsonTypeReflector.GetAttribute(objectType); + if (containerAttribute?.NamingStrategyType != null) + { + INamingStrategy namingStrategy = JsonTypeReflector.GetContainerNamingStrategy(containerAttribute); + contract.PropertyNameResolver = s => namingStrategy.GetDictionaryKey(s); + } + else + { + contract.PropertyNameResolver = ResolveDictionaryKey; + } + contract.Properties.AddRange(CreateProperties(objectType, MemberSerialization.OptOut)); return contract; @@ -1387,22 +1409,50 @@ namespace Newtonsoft.Json.Serialization JsonRequiredAttribute requiredAttribute = JsonTypeReflector.GetAttribute(attributeProvider); string mappedName; + bool hasSpecifiedName; if (propertyAttribute != null && propertyAttribute.PropertyName != null) { mappedName = propertyAttribute.PropertyName; + hasSpecifiedName = true; } #if !NET20 else if (dataMemberAttribute != null && dataMemberAttribute.Name != null) { mappedName = dataMemberAttribute.Name; + hasSpecifiedName = true; } #endif else { mappedName = name; + hasSpecifiedName = false; } - property.PropertyName = ResolvePropertyName(mappedName); + JsonContainerAttribute containerAttribute = JsonTypeReflector.GetAttribute(declaringType); + + INamingStrategy namingStrategy; + if (propertyAttribute?.NamingStrategyType != null) + { + namingStrategy = JsonTypeReflector.CreateNamingStrategyInstance(propertyAttribute.NamingStrategyType, propertyAttribute.NamingStrategyParameters); + } + else if (containerAttribute?.NamingStrategyType != null) + { + namingStrategy = JsonTypeReflector.GetContainerNamingStrategy(containerAttribute); + } + else + { + namingStrategy = NamingStrategy; + } + + if (namingStrategy != null) + { + property.PropertyName = namingStrategy.GetPropertyName(mappedName, hasSpecifiedName); + } + else + { + property.PropertyName = ResolvePropertyName(mappedName); + } + property.UnderlyingName = name; bool hasMemberAttribute = false; @@ -1543,6 +1593,11 @@ namespace Newtonsoft.Json.Serialization /// Resolved name of the property. protected virtual string ResolvePropertyName(string propertyName) { + if (NamingStrategy != null) + { + return NamingStrategy.GetPropertyName(propertyName, false); + } + return propertyName; } @@ -1553,6 +1608,11 @@ namespace Newtonsoft.Json.Serialization /// Resolved key of the dictionary. protected virtual string ResolveDictionaryKey(string dictionaryKey) { + if (NamingStrategy != null) + { + return NamingStrategy.GetDictionaryKey(dictionaryKey); + } + return ResolvePropertyName(dictionaryKey); } diff --git a/Src/Newtonsoft.Json/Serialization/DefaultNamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/DefaultNamingStrategy.cs new file mode 100644 index 00000000..2ddec384 --- /dev/null +++ b/Src/Newtonsoft.Json/Serialization/DefaultNamingStrategy.cs @@ -0,0 +1,15 @@ +namespace Newtonsoft.Json.Serialization +{ + public class DefaultNamingStrategy : INamingStrategy + { + public string GetPropertyName(string s, bool hasSpecifiedName) + { + return s; + } + + public string GetDictionaryKey(string s) + { + return s; + } + } +} \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Serialization/INamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/INamingStrategy.cs new file mode 100644 index 00000000..3767be6f --- /dev/null +++ b/Src/Newtonsoft.Json/Serialization/INamingStrategy.cs @@ -0,0 +1,8 @@ +namespace Newtonsoft.Json.Serialization +{ + public interface INamingStrategy + { + string GetPropertyName(string s, bool hasSpecifiedName); + string GetDictionaryKey(string s); + } +} \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs b/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs index 0744b943..ac649dc5 100644 --- a/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs +++ b/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs @@ -56,8 +56,8 @@ namespace Newtonsoft.Json.Serialization public const string ShouldSerializePrefix = "ShouldSerialize"; public const string SpecifiedPostfix = "Specified"; - private static readonly ThreadSafeStore> JsonConverterCreatorCache = - new ThreadSafeStore>(GetJsonConverterCreator); + private static readonly ThreadSafeStore> CreatorCache = + new ThreadSafeStore>(GetCreator); #if !(NET20 || DOTNET) private static readonly ThreadSafeStore AssociatedMetadataTypesCache = new ThreadSafeStore(GetAssociateMetadataTypeFromAttribute); @@ -162,10 +162,10 @@ namespace Newtonsoft.Json.Serialization if (converterAttribute != null) { - Func creator = JsonConverterCreatorCache.Get(converterAttribute.ConverterType); + Func creator = CreatorCache.Get(converterAttribute.ConverterType); if (creator != null) { - return creator(converterAttribute.ConverterParameters); + return (JsonConverter)creator(converterAttribute.ConverterParameters); } } @@ -180,19 +180,36 @@ namespace Newtonsoft.Json.Serialization /// If null, the default constructor is used. public static JsonConverter CreateJsonConverterInstance(Type converterType, object[] converterArgs) { - Func converterCreator = JsonConverterCreatorCache.Get(converterType); - return converterCreator(converterArgs); + Func converterCreator = CreatorCache.Get(converterType); + return (JsonConverter)converterCreator(converterArgs); } - /// - /// Create a factory function that can be used to create instances of a JsonConverter described by the - /// argument type. The returned function can then be used to either invoke the converter's default ctor, or any - /// parameterized constructors by way of an object array. - /// - private static Func GetJsonConverterCreator(Type converterType) + public static INamingStrategy CreateNamingStrategyInstance(Type namingStrategyType, object[] converterArgs) { - Func defaultConstructor = (ReflectionUtils.HasDefaultConstructor(converterType, false)) - ? ReflectionDelegateFactory.CreateDefaultConstructor(converterType) + Func converterCreator = CreatorCache.Get(namingStrategyType); + return (INamingStrategy)converterCreator(converterArgs); + } + + public static INamingStrategy GetContainerNamingStrategy(JsonContainerAttribute containerAttribute) + { + if (containerAttribute.NamingStrategy == null) + { + if (containerAttribute.NamingStrategyType == null) + { + return null; + + } + + containerAttribute.NamingStrategy = CreateNamingStrategyInstance(containerAttribute.NamingStrategyType, containerAttribute.NamingStrategyParameters); + } + + return containerAttribute.NamingStrategy; + } + + private static Func GetCreator(Type type) + { + Func defaultConstructor = (ReflectionUtils.HasDefaultConstructor(type, false)) + ? ReflectionDelegateFactory.CreateDefaultConstructor(type) : null; return (parameters) => @@ -201,31 +218,30 @@ namespace Newtonsoft.Json.Serialization { if (parameters != null) { - ObjectConstructor parameterizedConstructor = null; Type[] paramTypes = parameters.Select(param => param.GetType()).ToArray(); - ConstructorInfo parameterizedConstructorInfo = converterType.GetConstructor(paramTypes); + ConstructorInfo parameterizedConstructorInfo = type.GetConstructor(paramTypes); if (null != parameterizedConstructorInfo) { - parameterizedConstructor = ReflectionDelegateFactory.CreateParameterizedConstructor(parameterizedConstructorInfo); - return (JsonConverter)parameterizedConstructor(parameters); + ObjectConstructor parameterizedConstructor = ReflectionDelegateFactory.CreateParameterizedConstructor(parameterizedConstructorInfo); + return parameterizedConstructor(parameters); } else { - throw new JsonException("No matching parameterized constructor found for '{0}'.".FormatWith(CultureInfo.InvariantCulture, converterType)); + throw new JsonException("No matching parameterized constructor found for '{0}'.".FormatWith(CultureInfo.InvariantCulture, type)); } } if (defaultConstructor == null) { - throw new JsonException("No parameterless constructor defined for '{0}'.".FormatWith(CultureInfo.InvariantCulture, converterType)); + throw new JsonException("No parameterless constructor defined for '{0}'.".FormatWith(CultureInfo.InvariantCulture, type)); } - return (JsonConverter)defaultConstructor(); + return defaultConstructor(); } catch (Exception ex) { - throw new JsonException("Error creating '{0}'.".FormatWith(CultureInfo.InvariantCulture, converterType), ex); + throw new JsonException("Error creating '{0}'.".FormatWith(CultureInfo.InvariantCulture, type), ex); } }; } From 02c6a1e247180c3c1acae64e4e8b79a47a3326fb Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 28 May 2016 22:58:05 +1200 Subject: [PATCH 2/6] -Initial naming strategy implementation --- Src/Newtonsoft.Json/JsonContainerAttribute.cs | 17 ++++++- Src/Newtonsoft.Json/JsonConverterAttribute.cs | 12 ++--- Src/Newtonsoft.Json/JsonPropertyAttribute.cs | 23 ++++++++-- .../Serialization/CamelCaseNamingStrategy.cs | 45 +++++++++++++++++-- .../Serialization/DefaultContractResolver.cs | 4 ++ .../Serialization/DefaultNamingStrategy.cs | 14 ++++++ .../Serialization/INamingStrategy.cs | 15 +++++++ 7 files changed, 114 insertions(+), 16 deletions(-) diff --git a/Src/Newtonsoft.Json/JsonContainerAttribute.cs b/Src/Newtonsoft.Json/JsonContainerAttribute.cs index bf894657..1ca25541 100644 --- a/Src/Newtonsoft.Json/JsonContainerAttribute.cs +++ b/Src/Newtonsoft.Json/JsonContainerAttribute.cs @@ -59,9 +59,9 @@ namespace Newtonsoft.Json public Type ItemConverterType { get; set; } /// - /// The parameter list to use when constructing the JsonConverter described by ItemConverterType. + /// The parameter list to use when constructing the described by ItemConverterType. /// If null, the default constructor is used. - /// When non-null, there must be a constructor defined in the JsonConverter that exactly matches the number, + /// When non-null, there must be a constructor defined in the that exactly matches the number, /// order, and type of these parameters. /// /// @@ -69,6 +69,10 @@ namespace Newtonsoft.Json /// public object[] ItemConverterParameters { get; set; } + /// + /// Gets the of the . + /// + /// The of the . public Type NamingStrategyType { get { return _namingStrategyType; } @@ -79,6 +83,15 @@ namespace Newtonsoft.Json } } + /// + /// The parameter list to use when constructing the described by NamingStrategyType. + /// If null, the default constructor is used. + /// When non-null, there must be a constructor defined in the that exactly matches the number, + /// order, and type of these parameters. + /// + /// + /// [JsonContainer(NamingStrategyType = typeof(MyNamingStrategy), NamingStrategyParameters = new object[] { 123, "Four" })] + /// public object[] NamingStrategyParameters { get { return _namingStrategyParameters; } diff --git a/Src/Newtonsoft.Json/JsonConverterAttribute.cs b/Src/Newtonsoft.Json/JsonConverterAttribute.cs index fdafb3a4..aea831f6 100644 --- a/Src/Newtonsoft.Json/JsonConverterAttribute.cs +++ b/Src/Newtonsoft.Json/JsonConverterAttribute.cs @@ -38,16 +38,16 @@ namespace Newtonsoft.Json private readonly Type _converterType; /// - /// Gets the of the converter. + /// Gets the of the . /// - /// The of the converter. + /// The of the . public Type ConverterType { get { return _converterType; } } /// - /// The parameter list to use when constructing the JsonConverter described by ConverterType. + /// The parameter list to use when constructing the described by ConverterType. /// If null, the default constructor is used. /// public object[] ConverterParameters { get; private set; } @@ -55,7 +55,7 @@ namespace Newtonsoft.Json /// /// Initializes a new instance of the class. /// - /// Type of the converter. + /// Type of the . public JsonConverterAttribute(Type converterType) { if (converterType == null) @@ -69,8 +69,8 @@ namespace Newtonsoft.Json /// /// Initializes a new instance of the class. /// - /// Type of the converter. - /// Parameter list to use when constructing the JsonConverter. Can be null. + /// Type of the . + /// Parameter list to use when constructing the . Can be null. public JsonConverterAttribute(Type converterType, params object[] converterParameters) : this(converterType) { diff --git a/Src/Newtonsoft.Json/JsonPropertyAttribute.cs b/Src/Newtonsoft.Json/JsonPropertyAttribute.cs index 211bd842..b2a38ffa 100644 --- a/Src/Newtonsoft.Json/JsonPropertyAttribute.cs +++ b/Src/Newtonsoft.Json/JsonPropertyAttribute.cs @@ -24,6 +24,7 @@ #endregion using System; +using Newtonsoft.Json.Serialization; namespace Newtonsoft.Json { @@ -48,15 +49,15 @@ namespace Newtonsoft.Json internal TypeNameHandling? _itemTypeNameHandling; /// - /// Gets or sets the converter used when serializing the property's collection items. + /// Gets or sets the used when serializing the property's collection items. /// - /// The collection's items converter. + /// The collection's items . public Type ItemConverterType { get; set; } /// - /// The parameter list to use when constructing the JsonConverter described by ItemConverterType. + /// The parameter list to use when constructing the described by ItemConverterType. /// If null, the default constructor is used. - /// When non-null, there must be a constructor defined in the JsonConverter that exactly matches the number, + /// When non-null, there must be a constructor defined in the that exactly matches the number, /// order, and type of these parameters. /// /// @@ -64,7 +65,21 @@ namespace Newtonsoft.Json /// public object[] ItemConverterParameters { get; set; } + /// + /// Gets the of the . + /// + /// The of the . public Type NamingStrategyType { get; set; } + + /// + /// The parameter list to use when constructing the described by NamingStrategyType. + /// If null, the default constructor is used. + /// When non-null, there must be a constructor defined in the that exactly matches the number, + /// order, and type of these parameters. + /// + /// + /// [JsonProperty(NamingStrategyType = typeof(MyNamingStrategy), NamingStrategyParameters = new object[] { 123, "Four" })] + /// public object[] NamingStrategyParameters { get; set; } /// diff --git a/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs index a2d8a8c8..9a665b76 100644 --- a/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs +++ b/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs @@ -2,21 +2,53 @@ namespace Newtonsoft.Json.Serialization { + /// + /// A camel case naming strategy. + /// public class CamelCaseNamingStrategy : INamingStrategy { + /// + /// A flag indicating whether dictionary keys should be camel cased. + /// Defaults to false. + /// public bool CamelCaseDictionaryKeys { get; set; } + + /// + /// A flag indicating whether explicitly specified property names, + /// e.g. a property name customized with a , should be camel cased. + /// Defaults to false. + /// public bool OverrideSpecifiedNames { get; set; } - public CamelCaseNamingStrategy() - { - } - + /// + /// Initializes a new instance of the class. + /// + /// + /// A flag indicating whether dictionary keys should be camel cased. + /// + /// + /// A flag indicating whether explicitly specified property names, + /// e.g. a property name customized with a , should be camel cased. + /// public CamelCaseNamingStrategy(bool camelCaseDictionaryKeys, bool overrideSpecifiedNames) { CamelCaseDictionaryKeys = camelCaseDictionaryKeys; OverrideSpecifiedNames = overrideSpecifiedNames; } + /// + /// Initializes a new instance of the class. + /// + public CamelCaseNamingStrategy() + { + } + + /// + /// Gets the serialized name for a given property name. + /// + /// The initial property name. + /// A flag indicating whether the property has had a name explicitly specfied. + /// A property name. public string GetPropertyName(string s, bool hasSpecifiedName) { if (hasSpecifiedName && !OverrideSpecifiedNames) @@ -27,6 +59,11 @@ namespace Newtonsoft.Json.Serialization return StringUtils.ToCamelCase(s); } + /// + /// Gets the serialized key for a given dictionary key. + /// + /// The initial dictionary key. + /// A dictionary key. public string GetDictionaryKey(string s) { if (!CamelCaseDictionaryKeys) diff --git a/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs b/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs index 199f925b..e42ee67b 100644 --- a/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs +++ b/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs @@ -184,6 +184,10 @@ namespace Newtonsoft.Json.Serialization public bool IgnoreSerializableAttribute { get; set; } #endif + /// + /// Gets or sets the naming strategy used to resolve how property names and dictionary keys are serialized. + /// + /// The naming strategy used to resolve how property names and dictionary keys are serialized. public INamingStrategy NamingStrategy { get; set; } /// diff --git a/Src/Newtonsoft.Json/Serialization/DefaultNamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/DefaultNamingStrategy.cs index 2ddec384..c8f6ef2a 100644 --- a/Src/Newtonsoft.Json/Serialization/DefaultNamingStrategy.cs +++ b/Src/Newtonsoft.Json/Serialization/DefaultNamingStrategy.cs @@ -1,12 +1,26 @@ namespace Newtonsoft.Json.Serialization { + /// + /// The default naming strategy. Property names and dictionary keys are unchanged. + /// public class DefaultNamingStrategy : INamingStrategy { + /// + /// Gets the serialized name for a given property name. + /// + /// The initial property name. + /// A flag indicating whether the property has had a name explicitly specfied. + /// A property name. public string GetPropertyName(string s, bool hasSpecifiedName) { return s; } + /// + /// Gets the serialized key for a given dictionary key. + /// + /// The initial dictionary key. + /// A dictionary key. public string GetDictionaryKey(string s) { return s; diff --git a/Src/Newtonsoft.Json/Serialization/INamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/INamingStrategy.cs index 3767be6f..c59f18ef 100644 --- a/Src/Newtonsoft.Json/Serialization/INamingStrategy.cs +++ b/Src/Newtonsoft.Json/Serialization/INamingStrategy.cs @@ -1,8 +1,23 @@ namespace Newtonsoft.Json.Serialization { + /// + /// An interface for resolving how property names and dictionary keys are serialized. + /// public interface INamingStrategy { + /// + /// Gets the serialized name for a given property name. + /// + /// The initial property name. + /// A flag indicating whether the property has had a name explicitly specfied. + /// A property name. string GetPropertyName(string s, bool hasSpecifiedName); + + /// + /// Gets the serialized key for a given dictionary key. + /// + /// The initial dictionary key. + /// A dictionary key. string GetDictionaryKey(string s); } } \ No newline at end of file From 52fbb613d7c9d7a44179b5862e326483d2460721 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 28 May 2016 23:56:58 +1200 Subject: [PATCH 3/6] -Refactored naming strategy to be an abstract base class --- .../Newtonsoft.Json.Tests.Net20.csproj | 1 + .../Newtonsoft.Json.Tests.Net35.csproj | 1 + .../Newtonsoft.Json.Tests.Net40.csproj | 1 + .../Newtonsoft.Json.Tests.Portable.csproj | 1 + .../Newtonsoft.Json.Tests.Portable40.csproj | 1 + .../Newtonsoft.Json.Tests.csproj | 1 + .../CamelCaseNamingStrategyTests.cs | 4 +- .../SnakeCaseNamingStrategyTests.cs | 327 ++++++++++++++++++ .../Utilities/StringUtilsTests.cs | 33 +- Src/Newtonsoft.Json/JsonContainerAttribute.cs | 10 +- Src/Newtonsoft.Json/JsonPropertyAttribute.cs | 8 +- .../Newtonsoft.Json.Net20.csproj | 3 +- .../Newtonsoft.Json.Net35.csproj | 3 +- .../Newtonsoft.Json.Net40.csproj | 3 +- .../Newtonsoft.Json.Portable.csproj | 3 +- .../Newtonsoft.Json.Portable40.csproj | 3 +- Src/Newtonsoft.Json/Newtonsoft.Json.csproj | 3 +- .../Serialization/CamelCaseNamingStrategy.cs | 58 +--- .../CamelCasePropertyNamesContractResolver.cs | 2 +- .../Serialization/DefaultContractResolver.cs | 8 +- .../Serialization/DefaultNamingStrategy.cs | 23 +- .../Serialization/INamingStrategy.cs | 23 -- .../Serialization/JsonTypeReflector.cs | 6 +- .../Serialization/NamingStrategy.cs | 59 ++++ .../Serialization/SnakeCaseNamingStrategy.cs | 43 +++ Src/Newtonsoft.Json/Utilities/StringUtils.cs | 80 ++++- 26 files changed, 594 insertions(+), 114 deletions(-) create mode 100644 Src/Newtonsoft.Json.Tests/Serialization/SnakeCaseNamingStrategyTests.cs delete mode 100644 Src/Newtonsoft.Json/Serialization/INamingStrategy.cs create mode 100644 Src/Newtonsoft.Json/Serialization/NamingStrategy.cs create mode 100644 Src/Newtonsoft.Json/Serialization/SnakeCaseNamingStrategy.cs diff --git a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net20.csproj b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net20.csproj index c8c6da9b..4fa4587a 100644 --- a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net20.csproj +++ b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net20.csproj @@ -113,6 +113,7 @@ + diff --git a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net35.csproj b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net35.csproj index 9c34acac..a1f79940 100644 --- a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net35.csproj +++ b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net35.csproj @@ -156,6 +156,7 @@ + diff --git a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net40.csproj b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net40.csproj index fc5de9b0..e9635d0e 100644 --- a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net40.csproj +++ b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Net40.csproj @@ -272,6 +272,7 @@ + diff --git a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable.csproj b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable.csproj index d1fcb3f5..2be23499 100644 --- a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable.csproj +++ b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable.csproj @@ -147,6 +147,7 @@ + diff --git a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable40.csproj b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable40.csproj index ddc70846..2eda8590 100644 --- a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable40.csproj +++ b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.Portable40.csproj @@ -151,6 +151,7 @@ + diff --git a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj index c8e864e2..5eb93ab5 100644 --- a/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj +++ b/Src/Newtonsoft.Json.Tests/Newtonsoft.Json.Tests.csproj @@ -262,6 +262,7 @@ + diff --git a/Src/Newtonsoft.Json.Tests/Serialization/CamelCaseNamingStrategyTests.cs b/Src/Newtonsoft.Json.Tests/Serialization/CamelCaseNamingStrategyTests.cs index 392933ea..d0822a3b 100644 --- a/Src/Newtonsoft.Json.Tests/Serialization/CamelCaseNamingStrategyTests.cs +++ b/Src/Newtonsoft.Json.Tests/Serialization/CamelCaseNamingStrategyTests.cs @@ -174,7 +174,7 @@ namespace Newtonsoft.Json.Tests.Serialization { NamingStrategy = new CamelCaseNamingStrategy { - CamelCaseDictionaryKeys = true + ProcessDictionaryKeys = true } }; @@ -233,7 +233,7 @@ namespace Newtonsoft.Json.Tests.Serialization { NamingStrategy = new CamelCaseNamingStrategy { - CamelCaseDictionaryKeys = true + ProcessDictionaryKeys = true } }; diff --git a/Src/Newtonsoft.Json.Tests/Serialization/SnakeCaseNamingStrategyTests.cs b/Src/Newtonsoft.Json.Tests/Serialization/SnakeCaseNamingStrategyTests.cs new file mode 100644 index 00000000..9c254b7d --- /dev/null +++ b/Src/Newtonsoft.Json.Tests/Serialization/SnakeCaseNamingStrategyTests.cs @@ -0,0 +1,327 @@ +#region License +// Copyright (c) 2007 James Newton-King +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +#endregion + +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Serialization; +#if NETFX_CORE +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using TestFixture = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestClassAttribute; +using Test = Microsoft.VisualStudio.TestPlatform.UnitTestFramework.TestMethodAttribute; +#elif DNXCORE50 +using Xunit; +using Test = Xunit.FactAttribute; +using Assert = Newtonsoft.Json.Tests.XUnitAssert; +#else +using NUnit.Framework; +#endif +using Newtonsoft.Json.Tests.TestObjects; +using Newtonsoft.Json.Linq; +using System.Reflection; +using Newtonsoft.Json.Utilities; + +namespace Newtonsoft.Json.Tests.Serialization +{ + [TestFixture] + public class SnakeCaseNamingStrategyTests : TestFixtureBase + { + [Test] + public void JsonConvertSerializerSettings() + { + Person person = new Person(); + person.BirthDate = new DateTime(2000, 11, 20, 23, 55, 44, DateTimeKind.Utc); + person.LastModified = new DateTime(2000, 11, 20, 23, 55, 44, DateTimeKind.Utc); + person.Name = "Name!"; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + }; + + string json = JsonConvert.SerializeObject(person, Formatting.Indented, new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + StringAssert.AreEqual(@"{ + ""name"": ""Name!"", + ""birth_date"": ""2000-11-20T23:55:44Z"", + ""last_modified"": ""2000-11-20T23:55:44Z"" +}", json); + + Person deserializedPerson = JsonConvert.DeserializeObject(json, new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + Assert.AreEqual(person.BirthDate, deserializedPerson.BirthDate); + Assert.AreEqual(person.LastModified, deserializedPerson.LastModified); + Assert.AreEqual(person.Name, deserializedPerson.Name); + + json = JsonConvert.SerializeObject(person, Formatting.Indented); + StringAssert.AreEqual(@"{ + ""Name"": ""Name!"", + ""BirthDate"": ""2000-11-20T23:55:44Z"", + ""LastModified"": ""2000-11-20T23:55:44Z"" +}", json); + } + + [Test] + public void JTokenWriter_OverrideSpecifiedName() + { + JsonIgnoreAttributeOnClassTestClass ignoreAttributeOnClassTestClass = new JsonIgnoreAttributeOnClassTestClass(); + ignoreAttributeOnClassTestClass.Field = int.MinValue; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy + { + OverrideSpecifiedNames = true + } + }; + + JsonSerializer serializer = new JsonSerializer(); + serializer.ContractResolver = contractResolver; + + JTokenWriter writer = new JTokenWriter(); + + serializer.Serialize(writer, ignoreAttributeOnClassTestClass); + + JObject o = (JObject)writer.Token; + JProperty p = o.Property("the_field"); + + Assert.IsNotNull(p); + Assert.AreEqual(int.MinValue, (int)p.Value); + } + + [Test] + public void BlogPostExample() + { + Product product = new Product + { + ExpiryDate = new DateTime(2010, 12, 20, 18, 1, 0, DateTimeKind.Utc), + Name = "Widget", + Price = 9.99m, + Sizes = new[] { "Small", "Medium", "Large" } + }; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + }; + + string json = + JsonConvert.SerializeObject( + product, + Formatting.Indented, + new JsonSerializerSettings { ContractResolver = contractResolver } + ); + + //{ + // "name": "Widget", + // "expiryDate": "\/Date(1292868060000)\/", + // "price": 9.99, + // "sizes": [ + // "Small", + // "Medium", + // "Large" + // ] + //} + + StringAssert.AreEqual(@"{ + ""name"": ""Widget"", + ""expiry_date"": ""2010-12-20T18:01:00Z"", + ""price"": 9.99, + ""sizes"": [ + ""Small"", + ""Medium"", + ""Large"" + ] +}", json); + } + +#if !(NET35 || NET20 || PORTABLE40) + [Test] + public void DynamicSnakeCasePropertyNames() + { + dynamic o = new TestDynamicObject(); + o.Text = "Text!"; + o.Integer = int.MaxValue; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy + { + ProcessDictionaryKeys = true + } + }; + + string json = JsonConvert.SerializeObject(o, Formatting.Indented, + new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + StringAssert.AreEqual(@"{ + ""explicit"": false, + ""text"": ""Text!"", + ""integer"": 2147483647, + ""int"": 0, + ""child_object"": null +}", json); + } +#endif + + [Test] + public void DictionarySnakeCasePropertyNames_Disabled() + { + Dictionary values = new Dictionary + { + { "First", "Value1!" }, + { "Second", "Value2!" } + }; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + }; + + string json = JsonConvert.SerializeObject(values, Formatting.Indented, + new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + StringAssert.AreEqual(@"{ + ""First"": ""Value1!"", + ""Second"": ""Value2!"" +}", json); + } + + [Test] + public void DictionarySnakeCasePropertyNames_Enabled() + { + Dictionary values = new Dictionary + { + { "First", "Value1!" }, + { "Second", "Value2!" } + }; + + DefaultContractResolver contractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy + { + ProcessDictionaryKeys = true + } + }; + + string json = JsonConvert.SerializeObject(values, Formatting.Indented, + new JsonSerializerSettings + { + ContractResolver = contractResolver + }); + + StringAssert.AreEqual(@"{ + ""first"": ""Value1!"", + ""second"": ""Value2!"" +}", json); + } + + public class PropertyAttributeNamingStrategyTestClass + { + [JsonProperty] + public string HasNoAttributeNamingStrategy { get; set; } + + [JsonProperty(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] + public string HasAttributeNamingStrategy { get; set; } + } + + [Test] + public void JsonPropertyAttribute_NamingStrategyType() + { + PropertyAttributeNamingStrategyTestClass c = new PropertyAttributeNamingStrategyTestClass + { + HasNoAttributeNamingStrategy = "Value1!", + HasAttributeNamingStrategy = "Value2!" + }; + + string json = JsonConvert.SerializeObject(c, Formatting.Indented); + + StringAssert.AreEqual(@"{ + ""HasNoAttributeNamingStrategy"": ""Value1!"", + ""has_attribute_naming_strategy"": ""Value2!"" +}", json); + } + + [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] + public class ContainerAttributeNamingStrategyTestClass + { + public string Prop1 { get; set; } + public string Prop2 { get; set; } + [JsonProperty(NamingStrategyType = typeof(DefaultNamingStrategy))] + public string HasAttributeNamingStrategy { get; set; } + } + + [Test] + public void JsonObjectAttribute_NamingStrategyType() + { + ContainerAttributeNamingStrategyTestClass c = new ContainerAttributeNamingStrategyTestClass + { + Prop1 = "Value1!", + Prop2 = "Value2!" + }; + + string json = JsonConvert.SerializeObject(c, Formatting.Indented); + + StringAssert.AreEqual(@"{ + ""prop1"": ""Value1!"", + ""prop2"": ""Value2!"", + ""HasAttributeNamingStrategy"": null +}", json); + } + + [JsonDictionary(NamingStrategyType = typeof(SnakeCaseNamingStrategy), NamingStrategyParameters = new object[] { true, true })] + public class DictionaryAttributeNamingStrategyTestClass : Dictionary + { + } + + [Test] + public void JsonDictionaryAttribute_NamingStrategyType() + { + DictionaryAttributeNamingStrategyTestClass c = new DictionaryAttributeNamingStrategyTestClass + { + ["Key1"] = "Value1!", + ["Key2"] = "Value2!" + }; + + string json = JsonConvert.SerializeObject(c, Formatting.Indented); + + StringAssert.AreEqual(@"{ + ""key1"": ""Value1!"", + ""key2"": ""Value2!"" +}", json); + } + } +} \ No newline at end of file diff --git a/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs b/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs index 893776b1..fd0eb397 100644 --- a/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs +++ b/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs @@ -50,14 +50,45 @@ namespace Newtonsoft.Json.Tests.Utilities Assert.AreEqual("i", StringUtils.ToCamelCase("I")); Assert.AreEqual("", StringUtils.ToCamelCase("")); Assert.AreEqual(null, StringUtils.ToCamelCase(null)); - Assert.AreEqual("iPhone", StringUtils.ToCamelCase("iPhone")); Assert.AreEqual("person", StringUtils.ToCamelCase("Person")); + Assert.AreEqual("iPhone", StringUtils.ToCamelCase("iPhone")); Assert.AreEqual("iPhone", StringUtils.ToCamelCase("IPhone")); Assert.AreEqual("i Phone", StringUtils.ToCamelCase("I Phone")); + Assert.AreEqual("i Phone", StringUtils.ToCamelCase("I Phone")); Assert.AreEqual(" IPhone", StringUtils.ToCamelCase(" IPhone")); + Assert.AreEqual(" IPhone ", StringUtils.ToCamelCase(" IPhone ")); Assert.AreEqual("isCIA", StringUtils.ToCamelCase("IsCIA")); Assert.AreEqual("vmQ", StringUtils.ToCamelCase("VmQ")); Assert.AreEqual("xml2Json", StringUtils.ToCamelCase("Xml2Json")); + Assert.AreEqual("snAkEcAsE", StringUtils.ToCamelCase("SnAkEcAsE")); + Assert.AreEqual("snA__kEcAsE", StringUtils.ToCamelCase("SnA__kEcAsE")); + Assert.AreEqual("snA__ kEcAsE", StringUtils.ToCamelCase("SnA__ kEcAsE")); + Assert.AreEqual("already_snake_case_ ", StringUtils.ToCamelCase("already_snake_case_ ")); + } + + [Test] + public void ToSnakeCaseTest() + { + Assert.AreEqual("url_value", StringUtils.ToSnakeCase("URLValue")); + Assert.AreEqual("url", StringUtils.ToSnakeCase("URL")); + Assert.AreEqual("id", StringUtils.ToSnakeCase("ID")); + Assert.AreEqual("i", StringUtils.ToSnakeCase("I")); + Assert.AreEqual("", StringUtils.ToSnakeCase("")); + Assert.AreEqual(null, StringUtils.ToSnakeCase(null)); + Assert.AreEqual("person", StringUtils.ToSnakeCase("Person")); + Assert.AreEqual("i_phone", StringUtils.ToSnakeCase("iPhone")); + Assert.AreEqual("i_phone", StringUtils.ToSnakeCase("IPhone")); + Assert.AreEqual("i_phone", StringUtils.ToSnakeCase("I Phone")); + Assert.AreEqual("i_phone", StringUtils.ToSnakeCase("I Phone")); + Assert.AreEqual("i_phone", StringUtils.ToSnakeCase(" IPhone")); + Assert.AreEqual("i_phone", StringUtils.ToSnakeCase(" IPhone ")); + Assert.AreEqual("is_cia", StringUtils.ToSnakeCase("IsCIA")); + Assert.AreEqual("vm_q", StringUtils.ToSnakeCase("VmQ")); + Assert.AreEqual("xml2_json", StringUtils.ToSnakeCase("Xml2Json")); + Assert.AreEqual("sn_ak_ec_as_e", StringUtils.ToSnakeCase("SnAkEcAsE")); + Assert.AreEqual("sn_a__k_ec_as_e", StringUtils.ToSnakeCase("SnA__kEcAsE")); + Assert.AreEqual("sn_a__k_ec_as_e", StringUtils.ToSnakeCase("SnA__ kEcAsE")); + Assert.AreEqual("already_snake_case_", StringUtils.ToSnakeCase("already_snake_case_ ")); } } } \ No newline at end of file diff --git a/Src/Newtonsoft.Json/JsonContainerAttribute.cs b/Src/Newtonsoft.Json/JsonContainerAttribute.cs index 1ca25541..72d19424 100644 --- a/Src/Newtonsoft.Json/JsonContainerAttribute.cs +++ b/Src/Newtonsoft.Json/JsonContainerAttribute.cs @@ -70,9 +70,9 @@ namespace Newtonsoft.Json public object[] ItemConverterParameters { get; set; } /// - /// Gets the of the . + /// Gets the of the . /// - /// The of the . + /// The of the . public Type NamingStrategyType { get { return _namingStrategyType; } @@ -84,9 +84,9 @@ namespace Newtonsoft.Json } /// - /// The parameter list to use when constructing the described by NamingStrategyType. + /// The parameter list to use when constructing the described by NamingStrategyType. /// If null, the default constructor is used. - /// When non-null, there must be a constructor defined in the that exactly matches the number, + /// When non-null, there must be a constructor defined in the that exactly matches the number, /// order, and type of these parameters. /// /// @@ -102,7 +102,7 @@ namespace Newtonsoft.Json } } - internal INamingStrategy NamingStrategy { get; set; } + internal NamingStrategy NamingStrategy { get; set; } // yuck. can't set nullable properties on an attribute in C# // have to use this approach to get an unset default state diff --git a/Src/Newtonsoft.Json/JsonPropertyAttribute.cs b/Src/Newtonsoft.Json/JsonPropertyAttribute.cs index b2a38ffa..c9d2dae3 100644 --- a/Src/Newtonsoft.Json/JsonPropertyAttribute.cs +++ b/Src/Newtonsoft.Json/JsonPropertyAttribute.cs @@ -66,15 +66,15 @@ namespace Newtonsoft.Json public object[] ItemConverterParameters { get; set; } /// - /// Gets the of the . + /// Gets the of the . /// - /// The of the . + /// The of the . public Type NamingStrategyType { get; set; } /// - /// The parameter list to use when constructing the described by NamingStrategyType. + /// The parameter list to use when constructing the described by NamingStrategyType. /// If null, the default constructor is used. - /// When non-null, there must be a constructor defined in the that exactly matches the number, + /// When non-null, there must be a constructor defined in the that exactly matches the number, /// order, and type of these parameters. /// /// diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.Net20.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.Net20.csproj index 768041e0..436a3683 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.Net20.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.Net20.csproj @@ -133,12 +133,13 @@ - + + diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.Net35.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.Net35.csproj index 1330dc52..697a958c 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.Net35.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.Net35.csproj @@ -147,11 +147,12 @@ - + + diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.Net40.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.Net40.csproj index f78c0ef7..2e41fe86 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.Net40.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.Net40.csproj @@ -146,11 +146,12 @@ - + + diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.Portable.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.Portable.csproj index 963f2d2f..56f6b0ce 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.Portable.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.Portable.csproj @@ -177,7 +177,6 @@ - @@ -200,10 +199,12 @@ + + diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.Portable40.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.Portable40.csproj index 9ff94ac4..55c3494a 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.Portable40.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.Portable40.csproj @@ -182,7 +182,6 @@ - @@ -205,10 +204,12 @@ + + diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.csproj index 363c7dda..1055878b 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.csproj @@ -168,6 +168,7 @@ + @@ -181,7 +182,7 @@ - + diff --git a/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs index 9a665b76..80d1b9ac 100644 --- a/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs +++ b/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs @@ -5,34 +5,21 @@ namespace Newtonsoft.Json.Serialization /// /// A camel case naming strategy. /// - public class CamelCaseNamingStrategy : INamingStrategy + public class CamelCaseNamingStrategy : NamingStrategy { - /// - /// A flag indicating whether dictionary keys should be camel cased. - /// Defaults to false. - /// - public bool CamelCaseDictionaryKeys { get; set; } - - /// - /// A flag indicating whether explicitly specified property names, - /// e.g. a property name customized with a , should be camel cased. - /// Defaults to false. - /// - public bool OverrideSpecifiedNames { get; set; } - /// /// Initializes a new instance of the class. /// - /// - /// A flag indicating whether dictionary keys should be camel cased. + /// + /// A flag indicating whether dictionary keys should be processed. /// /// - /// A flag indicating whether explicitly specified property names, - /// e.g. a property name customized with a , should be camel cased. + /// A flag indicating whether explicitly specified property names should be processed, + /// e.g. a property name customized with a . /// - public CamelCaseNamingStrategy(bool camelCaseDictionaryKeys, bool overrideSpecifiedNames) + public CamelCaseNamingStrategy(bool processDictionaryKeys, bool overrideSpecifiedNames) { - CamelCaseDictionaryKeys = camelCaseDictionaryKeys; + ProcessDictionaryKeys = processDictionaryKeys; OverrideSpecifiedNames = overrideSpecifiedNames; } @@ -44,34 +31,13 @@ namespace Newtonsoft.Json.Serialization } /// - /// Gets the serialized name for a given property name. + /// Resolves the specified property name. /// - /// The initial property name. - /// A flag indicating whether the property has had a name explicitly specfied. - /// A property name. - public string GetPropertyName(string s, bool hasSpecifiedName) + /// The property name to resolve. + /// The resolved property name. + protected override string ResolvePropertyName(string name) { - if (hasSpecifiedName && !OverrideSpecifiedNames) - { - return s; - } - - return StringUtils.ToCamelCase(s); - } - - /// - /// Gets the serialized key for a given dictionary key. - /// - /// The initial dictionary key. - /// A dictionary key. - public string GetDictionaryKey(string s) - { - if (!CamelCaseDictionaryKeys) - { - return s; - } - - return StringUtils.ToCamelCase(s); + return StringUtils.ToCamelCase(name); } } } \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Serialization/CamelCasePropertyNamesContractResolver.cs b/Src/Newtonsoft.Json/Serialization/CamelCasePropertyNamesContractResolver.cs index 42061260..46085d58 100644 --- a/Src/Newtonsoft.Json/Serialization/CamelCasePropertyNamesContractResolver.cs +++ b/Src/Newtonsoft.Json/Serialization/CamelCasePropertyNamesContractResolver.cs @@ -43,7 +43,7 @@ namespace Newtonsoft.Json.Serialization { NamingStrategy = new CamelCaseNamingStrategy { - CamelCaseDictionaryKeys = true, + ProcessDictionaryKeys = true, OverrideSpecifiedNames = true }; } diff --git a/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs b/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs index e42ee67b..43a19c3e 100644 --- a/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs +++ b/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs @@ -188,7 +188,7 @@ namespace Newtonsoft.Json.Serialization /// Gets or sets the naming strategy used to resolve how property names and dictionary keys are serialized. /// /// The naming strategy used to resolve how property names and dictionary keys are serialized. - public INamingStrategy NamingStrategy { get; set; } + public NamingStrategy NamingStrategy { get; set; } /// /// Initializes a new instance of the class. @@ -947,7 +947,7 @@ namespace Newtonsoft.Json.Serialization JsonContainerAttribute containerAttribute = JsonTypeReflector.GetAttribute(objectType); if (containerAttribute?.NamingStrategyType != null) { - INamingStrategy namingStrategy = JsonTypeReflector.GetContainerNamingStrategy(containerAttribute); + NamingStrategy namingStrategy = JsonTypeReflector.GetContainerNamingStrategy(containerAttribute); contract.DictionaryKeyResolver = s => namingStrategy.GetDictionaryKey(s); } else @@ -1084,7 +1084,7 @@ namespace Newtonsoft.Json.Serialization JsonContainerAttribute containerAttribute = JsonTypeReflector.GetAttribute(objectType); if (containerAttribute?.NamingStrategyType != null) { - INamingStrategy namingStrategy = JsonTypeReflector.GetContainerNamingStrategy(containerAttribute); + NamingStrategy namingStrategy = JsonTypeReflector.GetContainerNamingStrategy(containerAttribute); contract.PropertyNameResolver = s => namingStrategy.GetDictionaryKey(s); } else @@ -1433,7 +1433,7 @@ namespace Newtonsoft.Json.Serialization JsonContainerAttribute containerAttribute = JsonTypeReflector.GetAttribute(declaringType); - INamingStrategy namingStrategy; + NamingStrategy namingStrategy; if (propertyAttribute?.NamingStrategyType != null) { namingStrategy = JsonTypeReflector.CreateNamingStrategyInstance(propertyAttribute.NamingStrategyType, propertyAttribute.NamingStrategyParameters); diff --git a/Src/Newtonsoft.Json/Serialization/DefaultNamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/DefaultNamingStrategy.cs index c8f6ef2a..0f60780b 100644 --- a/Src/Newtonsoft.Json/Serialization/DefaultNamingStrategy.cs +++ b/Src/Newtonsoft.Json/Serialization/DefaultNamingStrategy.cs @@ -3,27 +3,16 @@ /// /// The default naming strategy. Property names and dictionary keys are unchanged. /// - public class DefaultNamingStrategy : INamingStrategy + public class DefaultNamingStrategy : NamingStrategy { /// - /// Gets the serialized name for a given property name. + /// Resolves the specified property name. /// - /// The initial property name. - /// A flag indicating whether the property has had a name explicitly specfied. - /// A property name. - public string GetPropertyName(string s, bool hasSpecifiedName) + /// The property name to resolve. + /// The resolved property name. + protected override string ResolvePropertyName(string name) { - return s; - } - - /// - /// Gets the serialized key for a given dictionary key. - /// - /// The initial dictionary key. - /// A dictionary key. - public string GetDictionaryKey(string s) - { - return s; + return name; } } } \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Serialization/INamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/INamingStrategy.cs deleted file mode 100644 index c59f18ef..00000000 --- a/Src/Newtonsoft.Json/Serialization/INamingStrategy.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Newtonsoft.Json.Serialization -{ - /// - /// An interface for resolving how property names and dictionary keys are serialized. - /// - public interface INamingStrategy - { - /// - /// Gets the serialized name for a given property name. - /// - /// The initial property name. - /// A flag indicating whether the property has had a name explicitly specfied. - /// A property name. - string GetPropertyName(string s, bool hasSpecifiedName); - - /// - /// Gets the serialized key for a given dictionary key. - /// - /// The initial dictionary key. - /// A dictionary key. - string GetDictionaryKey(string s); - } -} \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs b/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs index ac649dc5..78cee84c 100644 --- a/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs +++ b/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs @@ -184,13 +184,13 @@ namespace Newtonsoft.Json.Serialization return (JsonConverter)converterCreator(converterArgs); } - public static INamingStrategy CreateNamingStrategyInstance(Type namingStrategyType, object[] converterArgs) + public static NamingStrategy CreateNamingStrategyInstance(Type namingStrategyType, object[] converterArgs) { Func converterCreator = CreatorCache.Get(namingStrategyType); - return (INamingStrategy)converterCreator(converterArgs); + return (NamingStrategy)converterCreator(converterArgs); } - public static INamingStrategy GetContainerNamingStrategy(JsonContainerAttribute containerAttribute) + public static NamingStrategy GetContainerNamingStrategy(JsonContainerAttribute containerAttribute) { if (containerAttribute.NamingStrategy == null) { diff --git a/Src/Newtonsoft.Json/Serialization/NamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/NamingStrategy.cs new file mode 100644 index 00000000..7c77b421 --- /dev/null +++ b/Src/Newtonsoft.Json/Serialization/NamingStrategy.cs @@ -0,0 +1,59 @@ +namespace Newtonsoft.Json.Serialization +{ + /// + /// A base class for resolving how property names and dictionary keys are serialized. + /// + public abstract class NamingStrategy + { + /// + /// A flag indicating whether dictionary keys should be processed. + /// Defaults to false. + /// + public bool ProcessDictionaryKeys { get; set; } + + /// + /// A flag indicating whether explicitly specified property names, + /// e.g. a property name customized with a , should be processed. + /// Defaults to false. + /// + public bool OverrideSpecifiedNames { get; set; } + + /// + /// Gets the serialized name for a given property name. + /// + /// The initial property name. + /// A flag indicating whether the property has had a name explicitly specfied. + /// A property name. + public virtual string GetPropertyName(string name, bool hasSpecifiedName) + { + if (hasSpecifiedName && !OverrideSpecifiedNames) + { + return name; + } + + return ResolvePropertyName(name); + } + + /// + /// Gets the serialized key for a given dictionary key. + /// + /// The initial dictionary key. + /// A dictionary key. + public virtual string GetDictionaryKey(string key) + { + if (!ProcessDictionaryKeys) + { + return key; + } + + return ResolvePropertyName(key); + } + + /// + /// Resolves the specified property name. + /// + /// The property name to resolve. + /// The resolved property name. + protected abstract string ResolvePropertyName(string name); + } +} \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Serialization/SnakeCaseNamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/SnakeCaseNamingStrategy.cs new file mode 100644 index 00000000..60fd9eef --- /dev/null +++ b/Src/Newtonsoft.Json/Serialization/SnakeCaseNamingStrategy.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json.Utilities; + +namespace Newtonsoft.Json.Serialization +{ + /// + /// A snake case naming strategy. + /// + public class SnakeCaseNamingStrategy : NamingStrategy + { + /// + /// Initializes a new instance of the class. + /// + /// + /// A flag indicating whether dictionary keys should be processed. + /// + /// + /// A flag indicating whether explicitly specified property names should be processed, + /// e.g. a property name customized with a . + /// + public SnakeCaseNamingStrategy(bool processDictionaryKeys, bool overrideSpecifiedNames) + { + ProcessDictionaryKeys = processDictionaryKeys; + OverrideSpecifiedNames = overrideSpecifiedNames; + } + + /// + /// Initializes a new instance of the class. + /// + public SnakeCaseNamingStrategy() + { + } + + /// + /// Resolves the specified property name. + /// + /// The property name to resolve. + /// The resolved property name. + protected override string ResolvePropertyName(string name) + { + return StringUtils.ToSnakeCase(name); + } + } +} \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Utilities/StringUtils.cs b/Src/Newtonsoft.Json/Utilities/StringUtils.cs index 09130116..fabf7c0e 100644 --- a/Src/Newtonsoft.Json/Utilities/StringUtils.cs +++ b/Src/Newtonsoft.Json/Utilities/StringUtils.cs @@ -190,16 +190,92 @@ namespace Newtonsoft.Json.Utilities break; } + char c; #if !(DOTNET || PORTABLE) - chars[i] = char.ToLower(chars[i], CultureInfo.InvariantCulture); + c = char.ToLower(chars[i], CultureInfo.InvariantCulture); #else - chars[i] = char.ToLowerInvariant(chars[i]); + c = char.ToLowerInvariant(chars[i]); #endif + chars[i] = c; } return new string(chars); } + internal enum SnakeCaseState + { + Start, + Lower, + Upper, + NewWord + } + + public static string ToSnakeCase(string s) + { + if (string.IsNullOrEmpty(s)) + { + return s; + } + + StringBuilder sb = new StringBuilder(); + SnakeCaseState state = SnakeCaseState.Start; + + for (int i = 0; i < s.Length; i++) + { + if (s[i] == ' ') + { + if (state != SnakeCaseState.Start) + { + state = SnakeCaseState.NewWord; + } + } + else if (char.IsUpper(s[i])) + { + switch (state) + { + case SnakeCaseState.Upper: + bool hasNext = (i + 1 < s.Length); + if (i > 0 && hasNext && !char.IsUpper(s[i + 1])) + { + sb.Append('_'); + } + break; + case SnakeCaseState.Lower: + case SnakeCaseState.NewWord: + sb.Append('_'); + break; + } + + char c; +#if !(DOTNET || PORTABLE) + c = char.ToLower(s[i], CultureInfo.InvariantCulture); +#else + c = char.ToLowerInvariant(s[i]); +#endif + sb.Append(c); + + state = SnakeCaseState.Upper; + } + else if (s[i] == '_') + { + sb.Append('_'); + state = SnakeCaseState.Start; + } + else + { + if (state == SnakeCaseState.NewWord) + { + sb.Append('_'); + } + + sb.Append(s[i]); + state = SnakeCaseState.Lower; + } + } + + return sb.ToString(); + } + public static bool IsHighSurrogate(char c) { #if !(PORTABLE40 || PORTABLE) From 1ae2912ee75b9028e65c9387f301e80d34235da5 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 29 May 2016 01:26:00 +1200 Subject: [PATCH 4/6] -Minor fixes --- Src/Newtonsoft.Json.Tests/PerformanceTests.cs | 2 +- .../Converters/IsoDateTimeConverter.cs | 2 +- Src/Newtonsoft.Json/JsonContainerAttribute.cs | 14 +++--- .../Serialization/JsonTypeReflector.cs | 7 ++- .../Utilities/JavaScriptUtils.cs | 2 +- Src/Newtonsoft.Json/Utilities/StringUtils.cs | 43 +++++-------------- 6 files changed, 23 insertions(+), 47 deletions(-) diff --git a/Src/Newtonsoft.Json.Tests/PerformanceTests.cs b/Src/Newtonsoft.Json.Tests/PerformanceTests.cs index 7fdd7ec7..475cc910 100644 --- a/Src/Newtonsoft.Json.Tests/PerformanceTests.cs +++ b/Src/Newtonsoft.Json.Tests/PerformanceTests.cs @@ -503,7 +503,7 @@ If attributes are not mentioned, default values are used in each case. { for (int i = 0; i < interations; i++) { - using (StringWriter w = StringUtils.CreateStringWriter(StringUtils.GetLength(text) ?? 16)) + using (StringWriter w = StringUtils.CreateStringWriter(text.Length)) { char[] buffer = null; JavaScriptUtils.WriteEscapedJavaScriptString(w, text, '"', true, JavaScriptUtils.DoubleQuoteCharEscapeFlags, StringEscapeHandling.Default, null, ref buffer); diff --git a/Src/Newtonsoft.Json/Converters/IsoDateTimeConverter.cs b/Src/Newtonsoft.Json/Converters/IsoDateTimeConverter.cs index 9d2f822a..ceb38797 100644 --- a/Src/Newtonsoft.Json/Converters/IsoDateTimeConverter.cs +++ b/Src/Newtonsoft.Json/Converters/IsoDateTimeConverter.cs @@ -57,7 +57,7 @@ namespace Newtonsoft.Json.Converters public string DateTimeFormat { get { return _dateTimeFormat ?? string.Empty; } - set { _dateTimeFormat = StringUtils.NullEmptyString(value); } + set { _dateTimeFormat = (string.IsNullOrEmpty(value)) ? null : value; } } /// diff --git a/Src/Newtonsoft.Json/JsonContainerAttribute.cs b/Src/Newtonsoft.Json/JsonContainerAttribute.cs index 72d19424..1deab53a 100644 --- a/Src/Newtonsoft.Json/JsonContainerAttribute.cs +++ b/Src/Newtonsoft.Json/JsonContainerAttribute.cs @@ -70,23 +70,23 @@ namespace Newtonsoft.Json public object[] ItemConverterParameters { get; set; } /// - /// Gets the of the . + /// Gets the of the . /// - /// The of the . + /// The of the . public Type NamingStrategyType { get { return _namingStrategyType; } set { _namingStrategyType = value; - NamingStrategy = null; + NamingStrategyInstance = null; } } /// - /// The parameter list to use when constructing the described by NamingStrategyType. + /// The parameter list to use when constructing the described by NamingStrategyType. /// If null, the default constructor is used. - /// When non-null, there must be a constructor defined in the that exactly matches the number, + /// When non-null, there must be a constructor defined in the that exactly matches the number, /// order, and type of these parameters. /// /// @@ -98,11 +98,11 @@ namespace Newtonsoft.Json set { _namingStrategyParameters = value; - NamingStrategy = null; + NamingStrategyInstance = null; } } - internal NamingStrategy NamingStrategy { get; set; } + internal NamingStrategy NamingStrategyInstance { get; set; } // yuck. can't set nullable properties on an attribute in C# // have to use this approach to get an unset default state diff --git a/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs b/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs index 78cee84c..ae1b5349 100644 --- a/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs +++ b/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs @@ -192,18 +192,17 @@ namespace Newtonsoft.Json.Serialization public static NamingStrategy GetContainerNamingStrategy(JsonContainerAttribute containerAttribute) { - if (containerAttribute.NamingStrategy == null) + if (containerAttribute.NamingStrategyInstance == null) { if (containerAttribute.NamingStrategyType == null) { return null; - } - containerAttribute.NamingStrategy = CreateNamingStrategyInstance(containerAttribute.NamingStrategyType, containerAttribute.NamingStrategyParameters); + containerAttribute.NamingStrategyInstance = CreateNamingStrategyInstance(containerAttribute.NamingStrategyType, containerAttribute.NamingStrategyParameters); } - return containerAttribute.NamingStrategy; + return containerAttribute.NamingStrategyInstance; } private static Func GetCreator(Type type) diff --git a/Src/Newtonsoft.Json/Utilities/JavaScriptUtils.cs b/Src/Newtonsoft.Json/Utilities/JavaScriptUtils.cs index e8ef53e7..887d9773 100644 --- a/Src/Newtonsoft.Json/Utilities/JavaScriptUtils.cs +++ b/Src/Newtonsoft.Json/Utilities/JavaScriptUtils.cs @@ -307,7 +307,7 @@ namespace Newtonsoft.Json.Utilities { bool[] charEscapeFlags = GetCharEscapeFlags(stringEscapeHandling, delimiter); - using (StringWriter w = StringUtils.CreateStringWriter(StringUtils.GetLength(value) ?? 16)) + using (StringWriter w = StringUtils.CreateStringWriter(value?.Length ?? 16)) { char[] buffer = null; WriteEscapedJavaScriptString(w, value, delimiter, appendDelimiters, charEscapeFlags, stringEscapeHandling, null, ref buffer); diff --git a/Src/Newtonsoft.Json/Utilities/StringUtils.cs b/Src/Newtonsoft.Json/Utilities/StringUtils.cs index fabf7c0e..54737636 100644 --- a/Src/Newtonsoft.Json/Utilities/StringUtils.cs +++ b/Src/Newtonsoft.Json/Utilities/StringUtils.cs @@ -33,7 +33,6 @@ using Newtonsoft.Json.Utilities.LinqBridge; #else using System.Linq; #endif -using Newtonsoft.Json.Serialization; namespace Newtonsoft.Json.Utilities { @@ -104,16 +103,6 @@ namespace Newtonsoft.Json.Utilities return true; } - /// - /// Nulls an empty string. - /// - /// The string. - /// Null if the string was null, otherwise the string unchanged. - public static string NullEmptyString(string s) - { - return (string.IsNullOrEmpty(s)) ? null : s; - } - public static StringWriter CreateStringWriter(int capacity) { StringBuilder sb = new StringBuilder(capacity); @@ -122,18 +111,6 @@ namespace Newtonsoft.Json.Utilities return sw; } - public static int? GetLength(string value) - { - if (value == null) - { - return null; - } - else - { - return value.Length; - } - } - public static void ToCharAsUnicode(char c, char[] buffer) { buffer[0] = '\\'; @@ -233,17 +210,17 @@ namespace Newtonsoft.Json.Utilities { switch (state) { - case SnakeCaseState.Upper: - bool hasNext = (i + 1 < s.Length); - if (i > 0 && hasNext && !char.IsUpper(s[i + 1])) - { + case SnakeCaseState.Upper: + bool hasNext = (i + 1 < s.Length); + if (i > 0 && hasNext && !char.IsUpper(s[i + 1])) + { + sb.Append('_'); + } + break; + case SnakeCaseState.Lower: + case SnakeCaseState.NewWord: sb.Append('_'); - } - break; - case SnakeCaseState.Lower: - case SnakeCaseState.NewWord: - sb.Append('_'); - break; + break; } char c; From 77a3a7d28d201c666b4a8adf87361ff0cb92d6d0 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 29 May 2016 01:31:52 +1200 Subject: [PATCH 5/6] -Derp --- Src/Newtonsoft.Json/Utilities/StringUtils.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Src/Newtonsoft.Json/Utilities/StringUtils.cs b/Src/Newtonsoft.Json/Utilities/StringUtils.cs index 54737636..d166b6ec 100644 --- a/Src/Newtonsoft.Json/Utilities/StringUtils.cs +++ b/Src/Newtonsoft.Json/Utilities/StringUtils.cs @@ -33,6 +33,7 @@ using Newtonsoft.Json.Utilities.LinqBridge; #else using System.Linq; #endif +using Newtonsoft.Json.Serialization; namespace Newtonsoft.Json.Utilities { From 9eb14a6590ad49d601dddc4a8f2e68ca98c2f2c4 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 31 May 2016 00:27:51 +1200 Subject: [PATCH 6/6] -Minor fixes --- Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs | 8 ++++++++ Src/Newtonsoft.Json/JsonContainerAttribute.cs | 4 ++-- Src/Newtonsoft.Json/JsonPropertyAttribute.cs | 2 +- Src/Newtonsoft.Json/Serialization/NamingStrategy.cs | 4 ++-- Src/Newtonsoft.Json/Utilities/StringUtils.cs | 8 ++++++-- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs b/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs index fd0eb397..bdc8e7e6 100644 --- a/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs +++ b/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs @@ -64,6 +64,10 @@ namespace Newtonsoft.Json.Tests.Utilities Assert.AreEqual("snA__kEcAsE", StringUtils.ToCamelCase("SnA__kEcAsE")); Assert.AreEqual("snA__ kEcAsE", StringUtils.ToCamelCase("SnA__ kEcAsE")); Assert.AreEqual("already_snake_case_ ", StringUtils.ToCamelCase("already_snake_case_ ")); + Assert.AreEqual("isJSONProperty", StringUtils.ToCamelCase("IsJSONProperty")); + Assert.AreEqual("shoutinG_CASE", StringUtils.ToCamelCase("SHOUTING_CASE")); + Assert.AreEqual("9999-12-31T23:59:59.9999999Z", StringUtils.ToCamelCase("9999-12-31T23:59:59.9999999Z")); + Assert.AreEqual("hi!! This is text. Time to test.", StringUtils.ToCamelCase("Hi!! This is text. Time to test.")); } [Test] @@ -89,6 +93,10 @@ namespace Newtonsoft.Json.Tests.Utilities Assert.AreEqual("sn_a__k_ec_as_e", StringUtils.ToSnakeCase("SnA__kEcAsE")); Assert.AreEqual("sn_a__k_ec_as_e", StringUtils.ToSnakeCase("SnA__ kEcAsE")); Assert.AreEqual("already_snake_case_", StringUtils.ToSnakeCase("already_snake_case_ ")); + Assert.AreEqual("is_json_property", StringUtils.ToSnakeCase("IsJSONProperty")); + Assert.AreEqual("shouting_case", StringUtils.ToSnakeCase("SHOUTING_CASE")); + Assert.AreEqual("9999-12-31_t23:59:59.9999999_z", StringUtils.ToSnakeCase("9999-12-31T23:59:59.9999999Z")); + Assert.AreEqual("hi!!_this_is_text._time_to_test.", StringUtils.ToSnakeCase("Hi!! This is text. Time to test.")); } } } \ No newline at end of file diff --git a/Src/Newtonsoft.Json/JsonContainerAttribute.cs b/Src/Newtonsoft.Json/JsonContainerAttribute.cs index 1deab53a..32bd975e 100644 --- a/Src/Newtonsoft.Json/JsonContainerAttribute.cs +++ b/Src/Newtonsoft.Json/JsonContainerAttribute.cs @@ -53,7 +53,7 @@ namespace Newtonsoft.Json public string Description { get; set; } /// - /// Gets the collection's items converter. + /// Gets or sets the collection's items converter. /// /// The collection's items converter. public Type ItemConverterType { get; set; } @@ -70,7 +70,7 @@ namespace Newtonsoft.Json public object[] ItemConverterParameters { get; set; } /// - /// Gets the of the . + /// Gets or sets the of the . /// /// The of the . public Type NamingStrategyType diff --git a/Src/Newtonsoft.Json/JsonPropertyAttribute.cs b/Src/Newtonsoft.Json/JsonPropertyAttribute.cs index c9d2dae3..0252ee44 100644 --- a/Src/Newtonsoft.Json/JsonPropertyAttribute.cs +++ b/Src/Newtonsoft.Json/JsonPropertyAttribute.cs @@ -66,7 +66,7 @@ namespace Newtonsoft.Json public object[] ItemConverterParameters { get; set; } /// - /// Gets the of the . + /// Gets or sets the of the . /// /// The of the . public Type NamingStrategyType { get; set; } diff --git a/Src/Newtonsoft.Json/Serialization/NamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/NamingStrategy.cs index 7c77b421..cf957af3 100644 --- a/Src/Newtonsoft.Json/Serialization/NamingStrategy.cs +++ b/Src/Newtonsoft.Json/Serialization/NamingStrategy.cs @@ -23,7 +23,7 @@ /// /// The initial property name. /// A flag indicating whether the property has had a name explicitly specfied. - /// A property name. + /// The serialized property name. public virtual string GetPropertyName(string name, bool hasSpecifiedName) { if (hasSpecifiedName && !OverrideSpecifiedNames) @@ -38,7 +38,7 @@ /// Gets the serialized key for a given dictionary key. /// /// The initial dictionary key. - /// A dictionary key. + /// The serialized dictionary key. public virtual string GetDictionaryKey(string key) { if (!ProcessDictionaryKeys) diff --git a/Src/Newtonsoft.Json/Utilities/StringUtils.cs b/Src/Newtonsoft.Json/Utilities/StringUtils.cs index d166b6ec..84e722e9 100644 --- a/Src/Newtonsoft.Json/Utilities/StringUtils.cs +++ b/Src/Newtonsoft.Json/Utilities/StringUtils.cs @@ -213,9 +213,13 @@ namespace Newtonsoft.Json.Utilities { case SnakeCaseState.Upper: bool hasNext = (i + 1 < s.Length); - if (i > 0 && hasNext && !char.IsUpper(s[i + 1])) + if (i > 0 && hasNext) { - sb.Append('_'); + char nextChar = s[i + 1]; + if (!char.IsUpper(nextChar) && nextChar != '_') + { + sb.Append('_'); + } } break; case SnakeCaseState.Lower: