Search code examples
c#jsonjson.net.net-4.6

Given an object graph composed of sub-classed of types which, how can we serialize that object to JSON while ignoring certain members of some types?


Given a C# object graph composed of subclasses of types which we cannot modify directly, what is the best strategy to serialize that object to JSON in .NET 4.6.2 while ignoring certain members of some classes?

Here is a concrete example of the object graph:

public class SomeBaseClass
{
    public int DontSerialize { get; set; }
}

public class SomeOtherBaseClass
{
    public int DontSerialize { get; set; }
}

public class YetAnotherBaseClass
{
    public int DontSerialize { get; set; }
}

public class SomeDerivedClass : SomeBaseClass
{
    public List<SomeOtherDerivedClass> SomeOtherDerivedCollection { get; set; }
    public YetAnotherDerivedClass YetAnotherDerivedClass { get; set; }
}

public class SomeOtherDerivedClass : SomeOtherBaseClass { }

public class YetAnotherDerivedClass : YetAnotherBaseClass { }

//serialize this
public class ObjectGraph
{
    public List<SomeDerivedClass> SomeDerivedCollection { get; set; }
}

The first thing we tried is tagging the derived types and their container with Serializable and the members we wish to ignore with JsonIgnore on a metadata type and tied the metadatatype to the derived type using the MetadataType attribute. Seemingly the Json Serializer did not detect or honor those attributes on derived classes.

I've tried developing a parameterized JsonConverter which gets a reference to the specific class it's meant to operate on as it's type parameter and variations on that theme. Depending on how I implement the WriteJson method and the CanConvert method I either get a stack overflow, or I miss some of the items in the graph.

I've also tried developing a custom attribute to tag each derived type indicating which members not to serialize with the same outcomes: skipped nodes or stackoverflow.

The root issue with the seems to be that calling any sort or serialization method from within WriteJson applies the same set of JsonConverters to the present object resulting in a cycle.

I've even tried adding and removing the current JsonConverter to avoid the recursion with no success.

Should it be possible to achieve this and if so what is the strategy?


Solution

  • Rather than a converter, you could use a custom contract resolver to ignore members of types that you cannot modify.

    First create the following contract resolver inheriting from DefaultContractResolver:

    public class DeclaredPropertyIgnoringContractResolver : DefaultContractResolver
    {
        // Ignore the properties with the specified declaring types and names when serializing and deserializing:
        readonly HashSet<ValueTuple<Type, string>> PropertiesToIgnore;
    
        public DeclaredPropertyIgnoringContractResolver(IEnumerable<ValueTuple<Type, string>> propertiesToIgnore)
        {
            this.PropertiesToIgnore = propertiesToIgnore.ToHashSet();
        }
        
        protected override JsonObjectContract CreateObjectContract(Type objectType)
        {
            var contract = base.CreateObjectContract(objectType);
            foreach (var property in contract.Properties)
                if (!property.Ignored && PropertiesToIgnore.Contains(ValueTuple.Create(property.DeclaringType, property.UnderlyingName)))
                    property.Ignored = true;
            return contract;
        }
    }
    

    Then create and cache a static instance such as the following:

    // Cache statically for best performance as recommended by https://www.newtonsoft.com/json/help/html/Performance.htm#ReuseContractResolver
    readonly static IContractResolver MyResolver = 
        new DeclaredPropertyIgnoringContractResolver(
        new ValueTuple<Type, string> []
        {
            ValueTuple.Create(typeof(SomeBaseClass), "DontSerialize"),
            ValueTuple.Create(typeof(SomeOtherBaseClass), "DontSerialize"),
            ValueTuple.Create(typeof(YetAnotherBaseClass), "DontSerialize"),
        }
    )
    {
        // Set other properties as required, e.g.:
        NamingStrategy = new CamelCaseNamingStrategy(),
    };
    

    Then finally serialize your model using the resolver in your JsonSerializerSettings e.g. as follows:

    var settings = new JsonSerializerSettings
    {
        ContractResolver = MyResolver,
        // Add other settings as required, e.g.
        NullValueHandling = NullValueHandling.Ignore,
    };
    
    var json = JsonConvert.SerializeObject(objectGraph, Formatting.Indented, settings);
    

    Demo fiddle #1 here.

    The resolver above assumes you want to ignore the DontSerialize properties when serializing instances of their declaring base types or any derived types. If you only want to ignore these properties when serializing specific derived types, use the following resolver:

    public class InheritedPropertyIgnoringContractResolver : DefaultContractResolver
    {
        // Ignore the properties with the specified declaring types and names when serializing and deserializing:
        readonly HashSet<ValueTuple<Type, string>> PropertiesToIgnore;
    
        public InheritedPropertyIgnoringContractResolver(IEnumerable<ValueTuple<Type, string>> propertiesToIgnore)
        {
            this.PropertiesToIgnore = propertiesToIgnore.ToHashSet();
        }
        
        protected override JsonObjectContract CreateObjectContract(Type objectType)
        {
            var contract = base.CreateObjectContract(objectType);
            foreach (var property in contract.Properties)
                if (!property.Ignored && PropertiesToIgnore.Contains(ValueTuple.Create(objectType, property.UnderlyingName)))
                    property.Ignored = true;
            return contract;
        }
    }
    

    And initialize MyResolver as follows, passing in the derived types instead of the base types:

    // Cache statically for best performance as recommended by https://www.newtonsoft.com/json/help/html/Performance.htm#ReuseContractResolver
    readonly static IContractResolver MyResolver = 
        new InheritedPropertyIgnoringContractResolver(
        new ValueTuple<Type, string> []
        {
            ValueTuple.Create(typeof(SomeDerivedClass), "DontSerialize"),
            ValueTuple.Create(typeof(SomeOtherDerivedClass), "DontSerialize"),
            ValueTuple.Create(typeof(YetAnotherDerivedClass), "DontSerialize"),
        }
    )
    {
        // Set other properties as required, e.g.:
        NamingStrategy = new CamelCaseNamingStrategy(),
    };
    

    Demo fiddle #2 here.

    Notes:

    • For a discussion of when to use a contract resolver instead of a converter, see JSON.net ContractResolver vs. JsonConverter.

    • Newtonsoft recommends to cache and reuse contract resolvers here: Performance Tips: Reuse Contract Resolver.

    • In current C# versions, one would use nameof(SomeBaseClass.DontSerialize) rather than a hardcoded string "DontSerialize". I'm not sure this was available in .NET 4.6.1 though.

      Similarly, in current versions one would use tuple syntax instead of ValueTuple.Create().