15

I'm using Newtonsoft's Json.NET 7.0.0.0 to serialize classes to JSON from C#:

class Foo
{
    public string X;
    public List<string> Y = new List<string>();
}

var json =
    JsonConvert.SerializeObject(
        new Foo(),
        Formatting.Indented,
        new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });

The value of json here is

{ "Y": [] }

but I would like it to be { } if Y is an empty list.

I couldn't find a satisfactory way to achieve this. Maybe with a custom contract resolver?

François Beaune
  • 4,270
  • 7
  • 41
  • 65
  • Also, I'd rather not add attributes on the collections as my classes have many of them, and all of them should be treated equally. – François Beaune Jan 20 '16 at 14:58
  • And you can't use the simple C# "if'? – st_stefanov Jan 20 '16 at 15:09
  • @st_stefanov How would that work if `Foo` has multiple collections, only some of them being empty, care to explain? :) – François Beaune Jan 20 '16 at 15:10
  • Ok, you want to serialize the class in any case, but handle the empty collections inside the class... – st_stefanov Jan 20 '16 at 15:13
  • And you didn't like this approach either? http://www.newtonsoft.com/json/help/html/ConditionalProperties.htm – st_stefanov Jan 20 '16 at 15:17
  • @st_stefanov I couldn't see how this approach could work in my case. It defines how one particular property of a class should be serialized. – François Beaune Jan 20 '16 at 15:20
  • It actually defines IF a property will be serialized. – st_stefanov Jan 20 '16 at 15:22
  • @st_stefanov Yes, but it does so for one specific property identified by its name. I'm looking for a solution that works for all collection fields and properties of all the classes (present and future) I need to serialize, without having to manually list them somewhere. The accepted solution is exactly what I was after. It's somewhat obscure but once written it can be tucked away and usage is straightforward. – François Beaune Jan 20 '16 at 16:41
  • Sure, I get it, I was thinking about a loop among the properties of type collection for your instance of the class, that way it would work for any number of properties. Anyway, may be I misread the suggested article. Glad you found your solution and good luck! – st_stefanov Jan 20 '16 at 19:08
  • @st_stefanov Thanks! – François Beaune Jan 20 '16 at 23:05

2 Answers2

14

If you're looking for a solution which can be used generically across different types and does not require any modification (attributes, etc), then the best solution that I can think if would be a custom DefaultContractResolver class. It would use reflection to determine if any IEnumerables for a given type are empty.

public class IgnoreEmptyEnumerablesResolver : DefaultContractResolver
{
    public static readonly IgnoreEmptyEnumerablesResolver Instance = new IgnoreEmptyEnumerablesResolver();

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);

        if (property.PropertyType != typeof(string) &&
            typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
        {
            property.ShouldSerialize = instance =>
            {
                IEnumerable enumerable = null;

                // this value could be in a public field or public property
                switch (member.MemberType)
                {
                    case MemberTypes.Property:
                        enumerable = instance
                            .GetType()
                            .GetProperty(member.Name)
                            .GetValue(instance, null) as IEnumerable;
                        break;
                    case MemberTypes.Field:
                        enumerable = instance
                            .GetType()
                            .GetField(member.Name)
                            .GetValue(instance) as IEnumerable;
                        break;
                    default:
                        break;

                }

                if (enumerable != null)
                {
                    // check to see if there is at least one item in the Enumerable
                    return enumerable.GetEnumerator().MoveNext();
                }
                else
                {
                    // if the list is null, we defer the decision to NullValueHandling
                    return true;
                }

            };
        }

        return property;
    }
}
Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
Will Ray
  • 10,621
  • 3
  • 46
  • 61
  • Thanks, that looks like exactly what I need, unfortunately it does not seem to work. It seems like `property.DeclaringType is IEnumerable` is always false. – François Beaune Jan 20 '16 at 15:29
  • You're right - there's a bug in the comparison. It's always looking at the declaring type, not the property type. I had left some code in that was resulting in a false positive on my end. Working on fixing it now. – Will Ray Jan 20 '16 at 15:50
  • 1
    Replace `property.DeclaringType is IEnumerable` by `typeof(IEnumerable).IsAssignableFrom(property.PropertyType) && property.PropertyType != typeof(string)` and `.GetProperty(property.PropertyName).GetValue(instance, null)` by `.GetField(property.PropertyName).GetValue(instance)` and then it works. – François Beaune Jan 20 '16 at 16:22
  • Yup! But do you also want to have this work on properties of a class as well as fields? I updated it to handle both. – Will Ray Jan 20 '16 at 16:24
  • I need both, I had made a similar modification to yours. Thank you! – François Beaune Jan 20 '16 at 16:31
  • One last thing: `property.PropertyName` doesn't have the right case if the contract resolver inherits from `CamelCasePropertyNamesContractResolver`. To fix the problem, use `member.Name` instead. – François Beaune Jan 20 '16 at 16:38
  • will this is susceptible to discarding items in an enumeration if it is not an in memory list. would u mind if i edit the answer to handle that? you can always revert/edit my change if u disagree – Simon Jan 17 '18 at 22:58
  • @Simon go for it! – Will Ray Jan 18 '18 at 02:15
  • 1
    I think the `enumerable` can be obtained in a much simpler way like this `property.ValueProvider?.GetValue(instance) as IEnumerable;` The `JsonProperty` already wraps an `IValueProvider` so that we don't have to play with lower level of reflection. – Hopeless Feb 12 '19 at 09:55
0

If you can modify your classes, you could add Shrink method and set null for all empty collections. It requires to change the class but it has better performance. Just another option for you.

quinvit
  • 319
  • 4
  • 6