11

I'm using Json.NET to convert a complex C# object graph to JSON. Due to ignoring properties which have default values in the object, I usually get empty object literals in the output, which I'd like to omit.

For example:

public class Sample {
  public int Value { get; set; }
  public string Name { get; set; }
}

public class ParentSample {
  // this property should never be null, hence the initializer
  public Sample Sample { get; } = new Sample();
}

..

var obj = new ParentSample();
// settings for indentation and excluding default values omitted for clarity
var output = JsonConvert.SerializeObject(obj, ... );
// output will be 
// {
//   Sample: {}
// }
//
// I'd like it to be 
// {}

I'm aware of some type specific solutions like adding a ShouldSerializeSample boolean method to the ParentSample type and check if all properties are default there. However I'd like a general solution in the form of a custom contract resolver for example.

Zoltán Tamási
  • 12,249
  • 8
  • 65
  • 93
  • You'll need to write something like a custom converter or `ShouldSerializeSample` for this. Since you have initialised ParentSample.Sample, Sample.Value will have a value of 0. How does the JsonConvert object know that this is 0 because it's default and not 0 because you purposefully set the Value as 0? – Fermin Apr 27 '16 at 09:35

3 Answers3

11

In the comments it looks like you have decided to resort to using Regex to get rid of the empty objects. One problem with that idea is it probably will not handle the situation where you have what I will call "recursive empty objects". In other words something like this:

{
    "foo":
    {
        "bar": {},
        "baz": {}
    }
}

If you manage to remove the deepest level empty objects bar and baz with Regex (while also realizing that you need to remove the comma between them to keep the JSON valid), you will still have an empty object left: foo.

{
    "foo":
    {
    }
}

I think a better solution is to load your data into a JToken hierarchy and then use a recursive method to remove all the empty children before writing it out to JSON. Something like this should work for your needs:

using System;
using Newtonsoft.Json.Linq;

public static class JsonHelper
{
    public static string SerializeToMinimalJson(object obj)
    {
        return JToken.FromObject(obj).RemoveEmptyChildren().ToString();
    }

    public static JToken RemoveEmptyChildren(this JToken token)
    {
        if (token.Type == JTokenType.Object)
        {
            JObject copy = new JObject();
            foreach (JProperty prop in token.Children<JProperty>())
            {
                JToken child = prop.Value;
                if (child.HasValues)
                {
                    child = child.RemoveEmptyChildren();
                }
                if (!child.IsEmptyOrDefault())
                {
                    copy.Add(prop.Name, child);
                }
            }
            return copy;
        }
        else if (token.Type == JTokenType.Array)
        {
            JArray copy = new JArray();
            foreach (JToken item in token.Children())
            {
                JToken child = item;
                if (child.HasValues)
                {
                    child = child.RemoveEmptyChildren();
                }
                if (!child.IsEmptyOrDefault())
                {
                    copy.Add(child);
                }
            }
            return copy;
        }
        return token;
    }

    public static bool IsEmptyOrDefault(this JToken token)
    {
        return (token.Type == JTokenType.Array && !token.HasValues) ||
               (token.Type == JTokenType.Object && !token.HasValues) ||
               (token.Type == JTokenType.String && token.ToString() == String.Empty) ||
               (token.Type == JTokenType.Boolean && token.Value<bool>() == false) ||
               (token.Type == JTokenType.Integer && token.Value<int>() == 0) ||
               (token.Type == JTokenType.Float && token.Value<double>() == 0.0) || 
               (token.Type == JTokenType.Null);
    }

}

You can then serialize your object(s) like this:

var json = JsonHelper.SerializeToMinimalJson(obj);

Fiddle: https://dotnetfiddle.net/awRPMR

EDIT

If you want to honor the [DefaultValue] attribute with this method, you can do so by modifying the SerializeToMinimalJson() method to create an instance of the JsonSerializer, setting the DefaultValueHandling property on it, and then passing it to JToken.FromObject() as shown below. (It has to be done this way because JTokens do not have references back to the original objects from which they were created using FromObject(), so there's no way to get the values of the [DefaultValue] attributes after that.)

public static string SerializeToMinimalJson(object obj)
{
    var serializer = new JsonSerializer();
    serializer.NullValueHandling = NullValueHandling.Ignore;
    serializer.DefaultValueHandling = DefaultValueHandling.Ignore;
    return JToken.FromObject(obj, serializer).RemoveEmptyChildren().ToString();
}

If you do that, you may also want to change the IsEmptyOrDefault() method so that it does not remove values that are the "default default". You can reduce it to this:

public static bool IsEmptyOrDefault(this JToken token)
{
    return (token.Type == JTokenType.Array && !token.HasValues) ||
           (token.Type == JTokenType.Object && !token.HasValues);
}

Fiddle: https://dotnetfiddle.net/0yVRI5

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • Thank you for the great answer, I wasn't aware of this kind of manipulation. One important question about your solution, is it possible to support `DefaultValueAttribute` when checking if a token value is "default"? – Zoltán Tamási Apr 28 '16 at 07:01
  • By the way, I solved the recursive empty object problem with putting the Regex solution into a loop. Kind of ugly and brute-force but for my current scenario it seemed to be acceptable. – Zoltán Tamási Apr 28 '16 at 07:02
  • @ZoltánTamási I've updated my answer to show how to make it respect the `DefaultValueAttribute`. Also, you're right, I suppose Regex would work if you applied it repeatedly in a loop until there were no more replacements to be made. Well, you are of course free to use whatever method works best for your situation. Now you have another tool in the toolbox. – Brian Rogers Apr 28 '16 at 15:10
  • @BrianRogers I've been trying to convert it to an iterative algorithm but I failed miserably, do you have any idea how to? Thanks! – aybe Feb 08 '21 at 10:01
  • @aybe If you can post your problem in a [new question](https://stackoverflow.com/questions/ask), we might be able to help. In the new question, you can include a [link](https://stackoverflow.com/q/36884902/10263) back to this question to help provide context. Be sure to post your code that isn't working, and tag it **json.net**. – Brian Rogers Feb 08 '21 at 15:53
  • @BrianRogers here it is : https://stackoverflow.com/questions/66097527/having-a-hard-time-converting-this-recursive-algorithm-to-an-iterative-one – aybe Feb 08 '21 at 20:43
  • @BrianRogers I've improved my question so it's easier to deal with, thanks for your time :) – aybe Feb 08 '21 at 23:12
0

You can give a JsonSerializerSettings to the method using NullValueHandling.Ignore:

var output = JsonConvert.SerializeObject(obj, new JsonSerializerSettings
{
    NullValueHandling = NullValueHandling.Ignore
});

If this settings doesn't give what you need, check: the documentation. There you can find all the properties and a description.

Edit: Using the child (Sample) as struct it works with DefaultValueHandling.Ignore. But @Zoltán Tamási will use a regex due to the class complexity.

aloisdg
  • 22,270
  • 6
  • 85
  • 105
  • Thanks for the answer, my problem is that the child object is not null, but treated as empty (because all of its properties have their default values). I think what you pointed out is something different. – Zoltán Tamási Apr 27 '16 at 09:21
  • You got the `DefaultValueHandling` – Oscar Vicente Perez Apr 27 '16 at 09:23
  • Yes, I'm using it to ignore default values, that's why the nested object gets empty in the JSON. But it cannot be used to treat the value of `Sample` as it were the default (because the default for a reference is `null`). I hope you understand what I mean – Zoltán Tamási Apr 27 '16 at 09:25
  • The problem with custom converters is that I have to write custom conversion to my whole object graph. I think i'll just end up with Regex replace empty object literal parts of the output. – Zoltán Tamási Apr 27 '16 at 09:38
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/110359/discussion-between-oscar-vicente-perez-and-zoltan-tamasi). – Oscar Vicente Perez Apr 27 '16 at 09:42
0

I implemented a slightly different solution which uses a generic method, reflection and some default Newtonsoft.Json ShouldSerialize functionality. Not elegant but conceptually simple for my particular need. Below is the LinqPad code snippet.

void Main()
{
    Person person = new Person();
    person.MyAddress = new Address();
    var ret = person.ShouldSerializeMyAddress();

    var json = JsonConvert.SerializeObject(person, Newtonsoft.Json.Formatting.Indented, new JsonSerializerSettings
    {
        NullValueHandling = NullValueHandling.Ignore
    });
    json.Dump();    
}

public static class JsonExtensions
{
    public static bool ShouldSerialize(this object self)
    {
        if (self == null)
            return false;

        var methods = self.GetType().GetMethods().Where(p => p.Name.StartsWith("ShouldSerialize"));
        return methods.Any(p => p.Invoke(self, null) is bool value && value);
    }
}

public class Person
{   
    public Address MyAddress { get; set; }  
    public bool ShouldSerializeMyAddress()
    {
        return MyAddress.ShouldSerialize();         
    }
}

public class Address
{
    public string Street { get; set; }
    public bool ShouldSerializeStreet()
    {
        return false;  // or whatever your property serialization criteria should be
    }
    public string City { get; set; }
    public bool ShouldSerializeCity()
    {
        return false;
    }
    public string State { get; set; }
    public bool ShouldSerializeState()
    {
        return false;
    }
    public string Zip { get; set; }
    public bool ShouldSerializeZip()
    {
        return false;
    }
}
PsiMatrix
  • 61
  • 8