So I have worked out an ugly solution that works to recursively reduce Json nodes that are empty while maintaining nested objects that are instantiated by default. The solution involves using a DefaultContractResolver implementation that uses recursion along with some type mapping to reduce the Json.
Here is the contract resolver:
public class ShouldSerializeContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
if (property.GetType().GetTypeName() == "object")
{
property.ShouldSerialize =
instance =>
{
var value = instance.GetType().GetProperty(property.UnderlyingName).GetValue(instance, null);
if (value == null)
{
return false;
}
if (value.GetType().GetTypeName() == "object")
{
if (NodeHasValue(value))
{
return true;
}
}
else
{
if (value.GetType().GetTypeName() == "collection")
{
ICollection enumerable = (ICollection)value;
if (enumerable.Count != 0)
{
return true;
}
else
{
return false;
}
}
return true;
}
return false;
};
}
return property;
}
public bool NodeHasValue(object obj)
{
Type objType = obj.GetType();
PropertyInfo[] properties = objType.GetProperties();
foreach (PropertyInfo property in properties)
{
var value = property.GetValue(obj, null);
if (value == null)
{
return false;
}
if (value.GetType().GetTypeName() == "object")
{
return NodeHasValue(value);
}
if (value.GetType().GetTypeName() == "collection")
{
ICollection enumerable = (ICollection)value;
if (enumerable.Count != 0)
{
return true;
}
}
if (value.GetType().GetTypeName() == "array")
{
IList enumerable = (IList)value;
if (enumerable.Count != 0)
{
return true;
}
}
if (value.GetType().GetTypeName() != "object"
&& value.GetType().GetTypeName() != "collection"
&& value.GetType().GetTypeName() != "array")
{
if (value != null)
{
var attribute = property.GetCustomAttribute(typeof(DefaultValueAttribute)) as DefaultValueAttribute;
if (attribute.Value.ToString() != value.ToString())
{
return true;
}
}
}
}
return false;
}
}
The method GetTypeName() is an extension method for the Type class, it works to identity what I specify are primitive types versus collections, objects, and arrays.
Extension method class for GetTypeName():
public static string GetTypeName(this Type type)
{
if (type.IsArray)
{
return "array";
}
if (type.GetTypeInfo().IsGenericType)
{
if (type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
return GetTypeName(Nullable.GetUnderlyingType(type)) + '?';
}
var genericTypeDefName = type.Name.Substring(0, type.Name.IndexOf('`'));
var genericTypeArguments = string.Join(", ", type.GenericTypeArguments.Select(GetTypeName));
if (type.GetTypeInfo().GetInterfaces().Any(ti => ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
{
return "collection";
}
return $"{genericTypeDefName}<{genericTypeArguments}>";
}
string typeName;
if (_primitiveTypeNames.TryGetValue(type, out typeName))
{
return typeName;
}
// enum's come up as a ValueType so we check IsEnum first.
if (type.GetTypeInfo().IsEnum)
{
return "enum";
}
if (type.GetTypeInfo().IsValueType)
{
return "struct";
}
return "object";
}
private static readonly Dictionary<Type, string> _primitiveTypeNames = new Dictionary<Type, string>
{
{ typeof(bool), "bool" },
{ typeof(byte), "byte" },
{ typeof(byte[]), "byte[]" },
{ typeof(sbyte), "sbyte" },
{ typeof(short), "short" },
{ typeof(ushort), "ushort" },
{ typeof(int), "int" },
{ typeof(uint), "uint" },
{ typeof(long), "long" },
{ typeof(ulong), "ulong" },
{ typeof(char), "char" },
{ typeof(float), "float" },
{ typeof(double), "double" },
{ typeof(string), "string" },
{ typeof(decimal), "decimal" }
};
}