From 52fbb613d7c9d7a44179b5862e326483d2460721 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 28 May 2016 23:56:58 +1200 Subject: [PATCH] -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)