Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serializer will now ignore: indexer properties, and properties with their name listed in a JsonIgnore above the declaring class #299

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -65,6 +67,8 @@ public void Setup()
{ DateTime.UtcNow },
{ TimeSpan.FromSeconds(100) }
};
ignoreTest = JsonIgnoreTestClass.CreateTestClass();
ignoreTestNoAttr = JsonIgnoreTestClassNoAttr.CreateTestClass();
}

[Benchmark]
Expand Down Expand Up @@ -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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add an extra line between the functions, so one is missing before the attribute. Same in the next few functions. It helps for readibility.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for more style errors. Even though I like organized code, I'm not used to checking that aspect quite so much. Plus it was a little messy from moving stuff around to get tests working. Will be better next time, don't want to waste your valuable time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing to apologize about! You're doing great! 👍🏻

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on José remark, you're doing great! No worry, you should have seen my very first PR on a .NET repository :-D Now, with habit, it's easy to spot them. We've been added StyleCop linter on the IoT repository as it's where we do have the most contributions. Not yet into those classes one. That will slowly come but it's not urgent. And anyway, we won't put it in place in the tests as it doesn't make too much sense.
So all good! You'll be indeed much better next time and at some point like us being able to spot those :-D
It's really to make it easier to read the code and navigate.

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);
});
}
}
}
63 changes: 63 additions & 0 deletions nanoFramework.Json.Test.Shared/JsonIgnoreTestClass.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Used to test the JsonIgnore attribute.
/// </summary>
[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);
}
}

/// <summary>
/// Used to 1-to-1 compare with JsonIgnoreTestClass.
/// </summary>
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
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Gender.cs" />
<Compile Include="$(MSBuildThisFileDirectory)JsonIgnoreTestClass.cs" />
<Compile Include="$(MSBuildThisFileDirectory)JsonTestClassChild.cs" />
<Compile Include="$(MSBuildThisFileDirectory)JsonTestClassComplex.cs" />
<Compile Include="$(MSBuildThisFileDirectory)JsonTestCompany.cs" />
Expand Down
28 changes: 28 additions & 0 deletions nanoFramework.Json.Test/JsonUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
icy3141 marked this conversation as resolved.
Show resolved Hide resolved
bool jsonSuccess = testObject.IsEqual(dserResult);
Assert.IsTrue(jsonSuccess);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that you can as well add an error message in case it's not successful. So you know where it broke and maybe other values you want to track in the test result.

OutputHelper.WriteLine("Serialization/Deserialization was " + (jsonSuccess ? "" : "NOT ") + "successful.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you arrive here, then it's always successful. So no need to test the value of jsonSuccess

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great to me now. So all up, as a summary, it seems we are good on performance side and you even managed to optimize a bit the existing code. Correct?

I think so, as long as my caching solution is acceptable. I will get to those last few fixes you suggested as soon as I can. The one thing I didn't do is run the performance tests with the code before I touched it. I can do that as well and provide my findings when I do my next push.


//test ignored properties are actually ignored
bool areIgnoredPropsPresent = jsonString.Contains("MyIgnoredProperty")
|| jsonString.Contains("AnotherIgnoredProperty");
Assert.IsFalse(areIgnoredPropsPresent);
OutputHelper.WriteLine("Ignore was " + (areIgnoredPropsPresent ? "NOT " : "") + "successful.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, the value will always be false


Json.Configuration.Settings.UseIgnoreAttribute = false;
OutputHelper.WriteLine("UseIgnoreAttribute set back to false.");
OutputHelper.WriteLine("Finished JsonIgnore Test.");
}
}

#region Test classes
Expand Down
5 changes: 5 additions & 0 deletions nanoFramework.Json/Configuration/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ public static class Settings
/// </summary>
public static bool CaseSensitive { get; set; } = true;

/// <summary>
/// If true, will check for JsonIgnoreAttribute upon serialization. Has a performance cost. Defaults to false.
/// </summary>
public static bool UseIgnoreAttribute { get; set; } = false;

/// <summary>
/// Gets or sets a value indicating whether deserialization should throw exception when no property found.
/// </summary>
Expand Down
33 changes: 33 additions & 0 deletions nanoFramework.Json/JsonIgnoreAttribute.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Hides properties from the json serializer.
/// </summary>
[System.AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public sealed class JsonIgnoreAttribute : Attribute
{
/// <summary>
/// Array of property names for json serializer to ignore.
/// </summary>
public string[] PropertyNames { get; set; }

/// <summary>
/// Hides properties from the json serializer.
/// </summary>
/// <param name="getterNamesToIgnore">A comma separated list of property names to ignore in json.</param>
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();
}
}
}
}
82 changes: 78 additions & 4 deletions nanoFramework.Json/JsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,26 +77,38 @@ 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
MethodInfo[] methods = type.GetMethods();

foreach (MethodInfo method in methods)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing here:
For each property you are creating gettersToIgnore based on classAttributes. Which means doing the same work for each property (foreach loop -> ShouldSerializeMethod -> ShouldIgnorePropertyFromClassAttribute -> creating array). Faster approach should be creating ignored property array before foreach (MethodInfo method in methods).

BTW @josesimoes does nanoFramework support attributes on properties?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@torbacz there is nothing preventing you from decorating properties with attributes.
I guess that you want to know if you can reach custom attributes for properties. That's a different story: that is not supported.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that was my question 😅 Because then we could just decorate each property. Also I'm not quite sure, if we should implement such solution. It's not resolving any problems. If you have class with properties which you don't want to send, just create derived class and pass it to JSON lib.
Don't get me wrong, I like new features but I'm concern about performance, adding new feature where there is a possibility for workaround is making lib much complicated over time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's wait for the benchmark results and then decide.
I do share your concern. Impacting performance is something to avoid at all costs. Unless the trade-off it's relevant.

As a side comment: I keep being surprised on how much interest this library has gathered from the community! And the investments on improving it are also surprising to me. 😄

{
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)
/// <summary>
/// Checks whether a property (MethodInfo) should be serialized.
/// </summary>
/// <param name="method">The MethodInfo to check.</param>
/// <param name="classAttributes">The cached class-level attributes. Only used if UseIgnoreAttribute is true.</param>
private static bool ShouldSerializeMethod(MethodInfo method, object[] classAttributes)
{
// We care only about property getters when serializing
if (!method.Name.StartsWith("get_"))
Expand Down Expand Up @@ -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;
icy3141 marked this conversation as resolved.
Show resolved Hide resolved
}

return true;
}

/// <summary>
/// Checks for JsonIgnore attribute on a method's declaring class. Helper method for SerializeClass.
/// </summary>
/// <param name="method">The MethodInfo of a property getter to check.</param>
/// <param name="classAttributes">The cached class-level attributes. Only used if UseIgnoreAttribute is true.</param>
/// <returns></returns>
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;
icy3141 marked this conversation as resolved.
Show resolved Hide resolved
}
}

return false;
icy3141 marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Extracts "get_" from MethodInfo.Name to retrieve the name of a getter property.
/// Assumes the MethodInfo is for a getter, checked elsewhere.
/// </summary>
/// <param name="getterMethodInfo">The MethodInfo of the getter property.</param>
/// <returns>The property name as it appears in written code.</returns>
private static string ExtractGetterName(MethodInfo getterMethodInfo)
{
// Substring(4) is to extract the "get_" for property methods
return getterMethodInfo.Name.Substring(4);
}

/// <summary>
/// Convert an IEnumerable to a JSON string.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions nanoFramework.Json/nanoFramework.Json.nfproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<Compile Include="Converters\UIntConverter.cs" />
<Compile Include="Converters\ULongConverter.cs" />
<Compile Include="Converters\UShortConverter.cs" />
<Compile Include="JsonIgnoreAttribute.cs" />
<Compile Include="JsonSerializer.cs" />
<Compile Include="Resolvers\IMemberResolver.cs" />
<Compile Include="Resolvers\MemberResolver.cs" />
Expand Down