diff --git a/Src/Newtonsoft.Json.Tests/Serialization/ExtensionDataTests.cs b/Src/Newtonsoft.Json.Tests/Serialization/ExtensionDataTests.cs index 8b3b79a8..8d04b22f 100644 --- a/Src/Newtonsoft.Json.Tests/Serialization/ExtensionDataTests.cs +++ b/Src/Newtonsoft.Json.Tests/Serialization/ExtensionDataTests.cs @@ -30,6 +30,7 @@ using System.Text; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Tests.TestObjects; using Newtonsoft.Json.Tests.TestObjects.Organization; +using Newtonsoft.Json.Serialization; #if DNXCORE50 using Xunit; using Test = Xunit.FactAttribute; @@ -312,6 +313,15 @@ namespace Newtonsoft.Json.Tests.Serialization } } + [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy), NamingStrategyParameters = new object[] { true, true, true })] + public class ExtensionDataWithNamingStrategyTestClass + { + public string Name { get; set; } + + [JsonExtensionData] + internal IDictionary ExtensionData { get; set; } + } + public class JObjectExtensionDataTestClass { public string Name { get; set; } @@ -371,6 +381,158 @@ namespace Newtonsoft.Json.Tests.Serialization Assert.AreEqual(7, c.ExtensionData.Count); } + [Test] + public void ExtensionDataTest_DeserializeWithNamingStrategy() + { + string json = @"{ + ""Ints"": [1,2,3], + ""Ignored"": [1,2,3], + ""Readonly"": ""Readonly"", + ""Name"": ""Actually set!"", + ""CustomName"": ""Wrong name!"", + ""GetPrivate"": true, + ""GetOnly"": true, + ""NewValueSimple"": true, + ""NewValueComplex"": [1,2,3] +}"; + + ExtensionDataTestClass c = JsonConvert.DeserializeObject(json, new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy + { + ProcessExtensionDataNames = true + } + } + }); + + Assert.AreEqual("Actually set!", c.Name); + Assert.AreEqual(4, c.Ints.Count); + + Assert.AreEqual("Readonly", (string)c.ExtensionData["Readonly"]); + Assert.AreEqual("Wrong name!", (string)c.ExtensionData["CustomName"]); + Assert.AreEqual(true, (bool)c.ExtensionData["GetPrivate"]); + Assert.AreEqual(true, (bool)c.ExtensionData["GetOnly"]); + Assert.AreEqual(true, (bool)c.ExtensionData["NewValueSimple"]); + Assert.IsTrue(JToken.DeepEquals(new JArray(1, 2, 3), c.ExtensionData["NewValueComplex"])); + Assert.IsTrue(JToken.DeepEquals(new JArray(1, 2, 3), c.ExtensionData["Ignored"])); + + Assert.AreEqual(7, c.ExtensionData.Count); + } + + [Test] + public void ExtensionDataTest_SerializeWithNamingStrategy_Enabled() + { + ExtensionDataTestClass c = new ExtensionDataTestClass() + { + ExtensionData = new Dictionary + { + ["TestValue1"] = 1, + ["alreadyCamelCase"] = new JObject + { + ["NotProcessed"] = true + } + } + }; + + string json = JsonConvert.SerializeObject(c, new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy + { + ProcessExtensionDataNames = true + } + }, + Formatting = Formatting.Indented + }); + + StringAssert.AreEqual(@"{ + ""readonly"": ""Readonly"", + ""name"": null, + ""custom_name"": null, + ""getPrivate"": false, + ""getOnly"": true, + ""ints"": [ + 0 + ], + ""testValue1"": 1, + ""alreadyCamelCase"": { + ""NotProcessed"": true + } +}", json); + } + + [Test] + public void ExtensionDataTest_SerializeWithNamingStrategy_Disabled() + { + ExtensionDataTestClass c = new ExtensionDataTestClass() + { + ExtensionData = new Dictionary + { + ["TestValue1"] = 1, + ["alreadyCamelCase"] = new JObject + { + ["NotProcessed"] = true + } + } + }; + + string json = JsonConvert.SerializeObject(c, new JsonSerializerSettings + { + ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver + { + NamingStrategy = new Newtonsoft.Json.Serialization.CamelCaseNamingStrategy() + }, + Formatting = Formatting.Indented + }); + + StringAssert.AreEqual(@"{ + ""readonly"": ""Readonly"", + ""name"": null, + ""custom_name"": null, + ""getPrivate"": false, + ""getOnly"": true, + ""ints"": [ + 0 + ], + ""TestValue1"": 1, + ""alreadyCamelCase"": { + ""NotProcessed"": true + } +}", json); + } + + [Test] + public void ExtensionDataTest_SerializeWithNamingStrategyAttribute() + { + ExtensionDataWithNamingStrategyTestClass c = new ExtensionDataWithNamingStrategyTestClass() + { + ExtensionData = new Dictionary + { + ["TestValue1"] = 1, + ["alreadyCamelCase"] = new JObject + { + ["NotProcessed"] = true + } + } + }; + + string json = JsonConvert.SerializeObject(c, new JsonSerializerSettings + { + Formatting = Formatting.Indented + }); + + StringAssert.AreEqual(@"{ + ""name"": null, + ""testValue1"": 1, + ""alreadyCamelCase"": { + ""NotProcessed"": true + } +}", json); + } + [JsonObject(MemberSerialization.OptIn)] public class MyClass { diff --git a/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs index 80d1b9ac..beb5066a 100644 --- a/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs +++ b/Src/Newtonsoft.Json/Serialization/CamelCaseNamingStrategy.cs @@ -23,6 +23,25 @@ namespace Newtonsoft.Json.Serialization OverrideSpecifiedNames = overrideSpecifiedNames; } + /// + /// 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 . + /// + /// + /// A flag indicating whether extension data names should be processed. + /// + public CamelCaseNamingStrategy(bool processDictionaryKeys, bool overrideSpecifiedNames, bool processExtensionDataNames) + : this(processDictionaryKeys, overrideSpecifiedNames) + { + ProcessExtensionDataNames = processExtensionDataNames; + } + /// /// Initializes a new instance of the class. /// diff --git a/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs b/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs index 11719554..4fc98035 100644 --- a/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs +++ b/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs @@ -398,12 +398,26 @@ namespace Newtonsoft.Json.Serialization contract.MemberSerialization = JsonTypeReflector.GetObjectMemberSerialization(contract.NonNullableUnderlyingType, ignoreSerializableAttribute); contract.Properties.AddRange(CreateProperties(contract.NonNullableUnderlyingType, contract.MemberSerialization)); + Func extensionDataNameResolver = null; + JsonObjectAttribute attribute = JsonTypeReflector.GetCachedAttribute(contract.NonNullableUnderlyingType); if (attribute != null) { contract.ItemRequired = attribute._itemRequired; + if (attribute.NamingStrategyType != null) + { + NamingStrategy namingStrategy = JsonTypeReflector.GetContainerNamingStrategy(attribute); + extensionDataNameResolver = s => namingStrategy.GetDictionaryKey(s); + } } + if (extensionDataNameResolver == null) + { + extensionDataNameResolver = ResolveExtensionDataName; + } + + contract.ExtensionDataNameResolver = extensionDataNameResolver; + if (contract.IsInstantiable) { ConstructorInfo overrideConstructor = GetAttributeConstructor(contract.NonNullableUnderlyingType); @@ -1604,6 +1618,21 @@ namespace Newtonsoft.Json.Serialization return propertyName; } + /// + /// Resolves the name of the extension data. By default no changes are made to extension data names. + /// + /// Name of the extension data. + /// Resolved name of the extension data. + protected virtual string ResolveExtensionDataName(string extensionDataName) + { + if (NamingStrategy != null) + { + return NamingStrategy.GetExtensionDataName(extensionDataName); + } + + return extensionDataName; + } + /// /// Resolves the key of the dictionary. By default is used to resolve dictionary keys. /// diff --git a/Src/Newtonsoft.Json/Serialization/JsonObjectContract.cs b/Src/Newtonsoft.Json/Serialization/JsonObjectContract.cs index ce03b2bd..807e3d7d 100644 --- a/Src/Newtonsoft.Json/Serialization/JsonObjectContract.cs +++ b/Src/Newtonsoft.Json/Serialization/JsonObjectContract.cs @@ -158,6 +158,12 @@ namespace Newtonsoft.Json.Serialization } } + /// + /// Gets or sets the extension data name resolver. + /// + /// The extension data name resolver. + public Func ExtensionDataNameResolver { get; set; } + internal bool ExtensionDataIsJToken; private bool? _hasRequiredOrDefaultValueProperties; private ConstructorInfo _parametrizedConstructor; diff --git a/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs b/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs index 8e38c792..d9b67165 100644 --- a/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs +++ b/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs @@ -496,6 +496,10 @@ namespace Newtonsoft.Json.Serialization bool escape; string propertyName = GetPropertyName(writer, e.Key, keyContract, out escape); + propertyName = (contract.ExtensionDataNameResolver != null) + ? contract.ExtensionDataNameResolver(propertyName) + : propertyName; + if (ShouldWriteReference(e.Value, null, valueContract, contract, member)) { writer.WritePropertyName(propertyName); diff --git a/Src/Newtonsoft.Json/Serialization/NamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/NamingStrategy.cs index a9e51e7b..695fdd06 100644 --- a/Src/Newtonsoft.Json/Serialization/NamingStrategy.cs +++ b/Src/Newtonsoft.Json/Serialization/NamingStrategy.cs @@ -11,6 +11,12 @@ /// public bool ProcessDictionaryKeys { get; set; } + /// + /// A flag indicating whether extension data names should be processed. + /// Defaults to false. + /// + public bool ProcessExtensionDataNames { get; set; } + /// /// A flag indicating whether explicitly specified property names, /// e.g. a property name customized with a , should be processed. @@ -34,6 +40,21 @@ return ResolvePropertyName(name); } + /// + /// Gets the serialized name for a given extension data name. + /// + /// The initial extension data name. + /// The serialized extension data name. + public virtual string GetExtensionDataName(string name) + { + if (!ProcessExtensionDataNames) + { + return name; + } + + return ResolvePropertyName(name); + } + /// /// Gets the serialized key for a given dictionary key. /// diff --git a/Src/Newtonsoft.Json/Serialization/SnakeCaseNamingStrategy.cs b/Src/Newtonsoft.Json/Serialization/SnakeCaseNamingStrategy.cs index 60fd9eef..7fec865f 100644 --- a/Src/Newtonsoft.Json/Serialization/SnakeCaseNamingStrategy.cs +++ b/Src/Newtonsoft.Json/Serialization/SnakeCaseNamingStrategy.cs @@ -23,6 +23,25 @@ namespace Newtonsoft.Json.Serialization OverrideSpecifiedNames = overrideSpecifiedNames; } + /// + /// 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 . + /// + /// + /// A flag indicating whether extension data names should be processed. + /// + public SnakeCaseNamingStrategy(bool processDictionaryKeys, bool overrideSpecifiedNames, bool processExtensionDataNames) + : this(processDictionaryKeys, overrideSpecifiedNames) + { + ProcessExtensionDataNames = processExtensionDataNames; + } + /// /// Initializes a new instance of the class. ///