diff --git a/nanoFramework.Json.Benchmark/SerializationBenchmarks/ReferenceTypesSerializationBenchmark.cs b/nanoFramework.Json.Benchmark/SerializationBenchmarks/ReferenceTypesSerializationBenchmark.cs index df6ff952..ccdfd728 100644 --- a/nanoFramework.Json.Benchmark/SerializationBenchmarks/ReferenceTypesSerializationBenchmark.cs +++ b/nanoFramework.Json.Benchmark/SerializationBenchmarks/ReferenceTypesSerializationBenchmark.cs @@ -23,6 +23,8 @@ public class ReferenceTypesSerializationBenchmark : BaseIterationBenchmark private JsonTestClassComplex complexClass; private JsonTestTown myTown; private ArrayList arrayList; + private JsonIgnoreTestClass ignoreTest; + private JsonIgnoreTestClassNoAttr ignoreTestNoAttr; [Setup] public void Setup() @@ -65,6 +67,8 @@ public void Setup() { DateTime.UtcNow }, { TimeSpan.FromSeconds(100) } }; + ignoreTest = JsonIgnoreTestClass.CreateTestClass(); + ignoreTestNoAttr = JsonIgnoreTestClassNoAttr.CreateTestClass(); } [Benchmark] @@ -120,5 +124,50 @@ public void ArrayList() JsonConvert.SerializeObject(arrayList); }); } + + [Benchmark] + public void ClassWithAttributeIgnoreEnabled() + { + RunInIteration(() => + { + // Turn ON the UseIgnore setting + Configuration.Settings.UseIgnoreAttribute = true; + JsonConvert.SerializeObject(ignoreTest); + // Turn OFF the UseIgnore setting + Configuration.Settings.UseIgnoreAttribute = false; + }); + } + [Benchmark] + public void ClassWithAttributeIgnoreDisabled() + { + RunInIteration(() => + { + // Turn OFF the UseIgnore setting + Configuration.Settings.UseIgnoreAttribute = false; + JsonConvert.SerializeObject(ignoreTest); + }); + } + [Benchmark] + public void ClassNoAttributeIgnoreEnabled() + { + RunInIteration(() => + { + // Turn ON the UseIgnore setting + Configuration.Settings.UseIgnoreAttribute = true; + JsonConvert.SerializeObject(ignoreTestNoAttr); + // Turn OFF the UseIgnore setting + Configuration.Settings.UseIgnoreAttribute = false; + }); + } + [Benchmark] + public void ClassNoAttributeIgnoreDisabled() + { + RunInIteration(() => + { + // Turn OFF the UseIgnore setting + Configuration.Settings.UseIgnoreAttribute = false; + JsonConvert.SerializeObject(ignoreTestNoAttr); + }); + } } } diff --git a/nanoFramework.Json.Test.Shared/JsonIgnoreTestClass.cs b/nanoFramework.Json.Test.Shared/JsonIgnoreTestClass.cs new file mode 100644 index 00000000..35be5c49 --- /dev/null +++ b/nanoFramework.Json.Test.Shared/JsonIgnoreTestClass.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +namespace nanoFramework.Json.Test.Shared +{ + /// + /// Used to test the JsonIgnore attribute. + /// + [JsonIgnore("MyIgnoredProperty, AnotherIgnoredProperty")] + public class JsonIgnoreTestClass + { + public int TestProperty { get; set; } + public int OtherTestProperty { get; set; } + public int AThirdTestProperty { get; set; } + public int MyIgnoredProperty => TestProperty + 1; + public int AnotherIgnoredProperty => OtherTestProperty + 1; + + public int this[int index] => TestProperty + index; + + public static JsonIgnoreTestClass CreateTestClass() + { + return new JsonIgnoreTestClass() + { + TestProperty = 1, + OtherTestProperty = 2, + AThirdTestProperty = 3 + }; + } + + public bool IsEqual(JsonIgnoreTestClass otherInstance) + { + return (TestProperty == otherInstance.TestProperty + && OtherTestProperty == otherInstance.OtherTestProperty + && AThirdTestProperty == otherInstance.AThirdTestProperty); + } + } + + /// + /// Used to 1-to-1 compare with JsonIgnoreTestClass. + /// + public class JsonIgnoreTestClassNoAttr + { + public int TestProperty { get; set; } + public int OtherTestProperty { get; set; } + public int AThirdTestProperty { get; set; } + public int MyIgnoredProperty => TestProperty + 1; + public int AnotherIgnoredProperty => OtherTestProperty + 1; + + public int this[int index] => TestProperty + index; + + public static JsonIgnoreTestClassNoAttr CreateTestClass() + { + return new JsonIgnoreTestClassNoAttr() + { + TestProperty = 1, + OtherTestProperty = 2, + AThirdTestProperty = 3 + }; + } + } +} \ No newline at end of file diff --git a/nanoFramework.Json.Test.Shared/nanoFramework.Json.Test.Shared.projitems b/nanoFramework.Json.Test.Shared/nanoFramework.Json.Test.Shared.projitems index 884026b0..0929649b 100644 --- a/nanoFramework.Json.Test.Shared/nanoFramework.Json.Test.Shared.projitems +++ b/nanoFramework.Json.Test.Shared/nanoFramework.Json.Test.Shared.projitems @@ -10,6 +10,7 @@ + diff --git a/nanoFramework.Json.Test/JsonUnitTests.cs b/nanoFramework.Json.Test/JsonUnitTests.cs index 819eec3c..f3e6d77c 100644 --- a/nanoFramework.Json.Test/JsonUnitTests.cs +++ b/nanoFramework.Json.Test/JsonUnitTests.cs @@ -1384,6 +1384,34 @@ public void DeserializeObjectWithStringContainingNonAsciiChars() Assert.AreEqual(input.Value, result.Value); } + [TestMethod] + public void DeserializeObjectWithJsonIgnoreAttribute() + { + OutputHelper.WriteLine("Starting JsonIgnore Test..."); + Json.Configuration.Settings.UseIgnoreAttribute = true; + OutputHelper.WriteLine("UseIgnoreAttribute enabled."); + + var testObject = JsonIgnoreTestClass.CreateTestClass(); + var jsonString = JsonSerializer.SerializeObject(testObject); + OutputHelper.WriteLine("After serialize."); + JsonIgnoreTestClass dserResult = JsonConvert.DeserializeObject(jsonString, typeof(JsonIgnoreTestClass)) as JsonIgnoreTestClass; + OutputHelper.WriteLine("After deserialize."); + + //test serialize and deserialize + bool jsonSuccess = testObject.IsEqual(dserResult); + Assert.IsTrue(jsonSuccess); + OutputHelper.WriteLine("Serialization/Deserialization was " + (jsonSuccess ? "" : "NOT ") + "successful."); + + //test ignored properties are actually ignored + bool areIgnoredPropsPresent = jsonString.Contains("MyIgnoredProperty") + || jsonString.Contains("AnotherIgnoredProperty"); + Assert.IsFalse(areIgnoredPropsPresent); + OutputHelper.WriteLine("Ignore was " + (areIgnoredPropsPresent ? "NOT " : "") + "successful."); + + Json.Configuration.Settings.UseIgnoreAttribute = false; + OutputHelper.WriteLine("UseIgnoreAttribute set back to false."); + OutputHelper.WriteLine("Finished JsonIgnore Test."); + } } #region Test classes diff --git a/nanoFramework.Json/Configuration/Settings.cs b/nanoFramework.Json/Configuration/Settings.cs index adb0637f..35c16c94 100644 --- a/nanoFramework.Json/Configuration/Settings.cs +++ b/nanoFramework.Json/Configuration/Settings.cs @@ -23,6 +23,11 @@ public static class Settings /// public static bool CaseSensitive { get; set; } = true; + /// + /// If true, will check for JsonIgnoreAttribute upon serialization. Has a performance cost. Defaults to false. + /// + public static bool UseIgnoreAttribute { get; set; } = false; + /// /// Gets or sets a value indicating whether deserialization should throw exception when no property found. /// diff --git a/nanoFramework.Json/JsonIgnoreAttribute.cs b/nanoFramework.Json/JsonIgnoreAttribute.cs new file mode 100644 index 00000000..b5f5c471 --- /dev/null +++ b/nanoFramework.Json/JsonIgnoreAttribute.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. + +using System; + +namespace nanoFramework.Json +{ + /// + /// Hides properties from the json serializer. + /// + [System.AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] + public sealed class JsonIgnoreAttribute : Attribute + { + /// + /// Array of property names for json serializer to ignore. + /// + public string[] PropertyNames { get; set; } + + /// + /// Hides properties from the json serializer. + /// + /// A comma separated list of property names to ignore in json. + public JsonIgnoreAttribute(string getterNamesToIgnore) + { + // Split by commas, then trim whitespace for each + PropertyNames = getterNamesToIgnore.Split(','); + for(int i = 0; i < PropertyNames.Length; i++) + { + PropertyNames[i] = PropertyNames[i].Trim(); + } + } + } +} diff --git a/nanoFramework.Json/JsonSerializer.cs b/nanoFramework.Json/JsonSerializer.cs index 9a44f845..481d660f 100644 --- a/nanoFramework.Json/JsonSerializer.cs +++ b/nanoFramework.Json/JsonSerializer.cs @@ -77,6 +77,13 @@ public static string SerializeObject(object o, bool topObject = true) private static string SerializeClass(object o, Type type) { + // Cache the type's class-level attributes only if UseIgnoreAttribute setting is enabled. + object[] classAttributes = null; + if (Settings.UseIgnoreAttribute) + { + classAttributes = type.GetCustomAttributes(false); + } + Hashtable hashtable = new(); // Iterate through all of the methods, looking for internal GET properties @@ -84,19 +91,24 @@ private static string SerializeClass(object o, Type type) foreach (MethodInfo method in methods) { - if (!ShouldSerializeMethod(method)) + if (!ShouldSerializeMethod(method, classAttributes)) { continue; } - + object returnObject = method.Invoke(o, null); - hashtable.Add(method.Name.Substring(4), returnObject); + hashtable.Add(ExtractGetterName(method), returnObject); } return SerializeIDictionary(hashtable); } - private static bool ShouldSerializeMethod(MethodInfo method) + /// + /// Checks whether a property (MethodInfo) should be serialized. + /// + /// The MethodInfo to check. + /// The cached class-level attributes. Only used if UseIgnoreAttribute is true. + private static bool ShouldSerializeMethod(MethodInfo method, object[] classAttributes) { // We care only about property getters when serializing if (!method.Name.StartsWith("get_")) @@ -125,9 +137,71 @@ private static bool ShouldSerializeMethod(MethodInfo method) return false; } + // Ignore indexer properties + // (string comparison is MUCH faster than method.GetParameters) + if (method.Name == "get_Item") + { + return false; + } + + // Ignore properties listed in [JsonIgnore()] attribute + // Only check for attribute if the setting is on + if (Settings.UseIgnoreAttribute && + ShouldIgnorePropertyFromClassAttribute(method, classAttributes)) + { + return false; + } + return true; } + /// + /// Checks for JsonIgnore attribute on a method's declaring class. Helper method for SerializeClass. + /// + /// The MethodInfo of a property getter to check. + /// The cached class-level attributes. Only used if UseIgnoreAttribute is true. + /// + private static bool ShouldIgnorePropertyFromClassAttribute(MethodInfo method, object[] classAttributes) + { + string[] gettersToIgnore = null; + + foreach (object attribute in classAttributes) + { + if (attribute is JsonIgnoreAttribute ignoreAttribute) + { + gettersToIgnore = ignoreAttribute.PropertyNames; + break; + } + } + + if (gettersToIgnore == null) + { + return false; + } + + foreach (string propertyName in gettersToIgnore) + { + if (propertyName.Equals(ExtractGetterName(method))) + { + return true; + } + } + + return false; + } + + /// + /// Extracts "get_" from MethodInfo.Name to retrieve the name of a getter property. + /// Assumes the MethodInfo is for a getter, checked elsewhere. + /// + /// The MethodInfo of the getter property. + /// The property name as it appears in written code. + private static string ExtractGetterName(MethodInfo getterMethodInfo) + { + // Substring(4) is to extract the "get_" for property methods + return getterMethodInfo.Name.Substring(4); + } + /// /// Convert an IEnumerable to a JSON string. /// diff --git a/nanoFramework.Json/nanoFramework.Json.nfproj b/nanoFramework.Json/nanoFramework.Json.nfproj index d72860e5..9a4f1d09 100644 --- a/nanoFramework.Json/nanoFramework.Json.nfproj +++ b/nanoFramework.Json/nanoFramework.Json.nfproj @@ -52,6 +52,7 @@ +