WebAPI JSON Serialization not serializing any children of a composite object

10,598

The children are not being serialized because the list of Elements in your Composite is private. Json.Net will not serialize private members by default. If you mark the list with [JsonProperty("Elements")] then the children will be serialized.

public class Composite: Element
{
   ...
   [JsonProperty("Elements")]
   private List<Element> Elements { get; set; }
   ...
}

If you run your example code with this change, you should get the following JSON:

{
  "Elements": [
    {
      "Elements": [
        {
          "Name": "Leaf1"
        },
        {
          "Name": "Leaf2"
        }
      ],
      "Name": "Branch"
    }
  ],
  "Name": "Root"
}

EDIT

OK, here is an example converter for your composite:

class CompositeConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(Composite));
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        Composite composite = (Composite)value;

        // Need to use reflection here because Elements is private
        PropertyInfo prop = typeof(Composite).GetProperty("Elements", BindingFlags.NonPublic | BindingFlags.Instance);
        List<Element> children = (List<Element>)prop.GetValue(composite);

        JArray array = new JArray();
        foreach (Element e in children)
        {
            array.Add(JToken.FromObject(e, serializer));
        }

        JObject obj = new JObject();
        obj.Add(composite.Name, array);
        obj.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Here is a demo:

class Program
{
    static void Main(string[] args)
    {
        Composite root = new Composite("Root");
        Composite branch1 = new Composite("Branch1");
        branch1.Add(new ConcreteElement("Leaf1", "Bar"));
        branch1.Add(new ConcreteElement("Leaf2", "Baz"));
        root.Add(branch1);
        Composite branch2 = new Composite("Branch2");
        branch2.Add(new ConcreteElement("Leaf3", "Quux"));
        Composite branch3 = new Composite("Branch3");
        branch3.Add(new ConcreteElement("Leaf4", "Fizz"));
        branch2.Add(branch3);
        root.Add(branch2);
        string json = JsonConvert.SerializeObject(root, Formatting.Indented, new CompositeConverter());
        Console.WriteLine(json);
    }
}

public abstract class Element
{
    protected string _name;
    public Element(string name)
    {
        _name = name;
    }
    public abstract void Add(Element element);
    public string Name { get { return _name; } }
}

public class ConcreteElement : Element
{
    public ConcreteElement(string name, string foo) : base(name)
    {
        Foo = foo;
    }
    public string Foo { get; set; }
    public override void Add(Element element)
    {
        throw new InvalidOperationException("ConcreteElements may not contain Child nodes. Perhaps you intended to add this to a Composite");
    }
}

public class Composite : Element
{
    public Composite(string name) : base(name) { Elements = new List<Element>(); }
    private List<Element> Elements { get; set; }
    public override void Add(Element element)
    {
        Elements.Add(element);
    }
}

Here is the resulting JSON output:

{
  "Root": [
    {
      "Branch1": [
        {
          "Foo": "Bar",
          "Name": "Leaf1"
        },
        {
          "Foo": "Baz",
          "Name": "Leaf2"
        }
      ]
    },
    {
      "Branch2": [
        {
          "Foo": "Quux",
          "Name": "Leaf3"
        },
        {
          "Branch3": [
            {
              "Foo": "Fizz",
              "Name": "Leaf4"
            }
          ]
        }
      ]
    }
  ]
}

I realize that this is not exactly the same JSON that you asked for, but it should get you going in the right direction. One problem with the "desired" JSON you specified in your question is that it is not entirely valid. Named properties can only be inside an object, not directly inside an array. In your second example, you have a named "Branch3" property directly inside the array for "Branch2". This won't work. So, you would need to make Branch2 an object instead. But if you do this, then you have an inconsistent representation for your composite: if it contains only leaves, then it is an array, otherwise it is an object. It is possible to make a converter to change the representation of the composite based on the contents (in fact I managed to create such a beast), but that makes the JSON more difficult to consume, and in the end I don't think you'll want to use it. In case you're curious, I've included this alternate converter below, along with its output.

class CompositeConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(Composite));
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        Composite composite = (Composite)value;

        // Need to use reflection here because Elements is private
        PropertyInfo prop = typeof(Composite).GetProperty("Elements", BindingFlags.NonPublic | BindingFlags.Instance);
        List<Element> children = (List<Element>)prop.GetValue(composite);

        // if all children are leaves, output as an array
        if (children.All(el => el.GetType() != typeof(Composite)))
        {
            JArray array = new JArray();
            foreach (Element e in children)
            {
                array.Add(JToken.FromObject(e, serializer));
            }
            array.WriteTo(writer);
        }
        else 
        {
            // otherwise use an object
            JObject obj = new JObject();
            if (composite.Name == "Root")
            {
                obj.Add("Name", composite.Name);
            }
            foreach (Element e in children)
            {
                obj.Add(e.Name, JToken.FromObject(e, serializer));
            }
            obj.WriteTo(writer);
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Output using the same data:

{
  "Name": "Root",
  "Branch1": [
    {
      "Foo": "Bar",
      "Name": "Leaf1"
    },
    {
      "Foo": "Baz",
      "Name": "Leaf2"
    }
  ],
  "Branch2": {
    "Leaf3": {
      "Foo": "Quux",
      "Name": "Leaf3"
    },
    "Branch3": [
      {
        "Foo": "Fizz",
        "Name": "Leaf4"
      }
    ]
  }
}
Share:
10,598
K. Alan Bates
Author by

K. Alan Bates

Updated on June 04, 2022

Comments

  • K. Alan Bates
    K. Alan Bates over 1 year

    So I am needing to serialize a composite to JSON (with JSON.NET) and was hoping that coming here with this problem would be a quick win.

    I have a very basic composite implementation that I am just trying to use to scaffold my services and the data structure but the JSONSerializer is only serializing the root node.

    Code:

    namespace Data
    {   
       public abstract class Element
       {
           protected string _name;
           public Element(string name)
           {
               _name = name;
           }
           public abstract void Add(Element element);
    
    
           public string Name { get { return _name; } }
       }
    
       public class ConcreteElement : Element
       {
          public ConcreteElement(string name) : base(name) { }
          public override void Add(Element element)
          {
             throw new InvalidOperationException("ConcreteElements may not contain Child nodes. Perhaps you intended to add this to a Composite");
          }
       }
    
        public class Composite: Element
        {
           public Composite(string name) : base(name) { Elements = new List<Element>(); }
           private List<Element> Elements { get; set; }
           public override void Add(Element element)
           {
               Elements.Add(element);
           }
        }
    }
    

    In my Controller's HttpGet method,

    Composite root = new Composite("Root");
    Composite branch = new Composite("Branch");
    branch.Add(new ConcreteElement("Leaf1"));
    branch.Add(new ConcreteElement("Leaf2"));
    root.Add(branch);
    return JsonConvert.SerializeObject(root);
    

    And the only thing that is being serialized is

    {"Name\":\"Root\"}"
    

    Can anyone see a reason that this is not serializing child elements? I'm hoping it's something stupid.

    Edit1

    I've never tried to Serialize a graph to JSON with WebAPI before. Do I need to write a custom MediaTypeFormatter for serializing this?

    Edit2 (to add desired output)

    Leaf1 and Leaf2 are just markers at the moment. They will themselves be complex objects once I can get this to serialize. So, at the moment...

    {
      "Name" : "Root"
      ,"Branch":  
               [
                  {"Name":"Leaf1"}
                 ,{"Name":"Leaf2"}
                 ]
               ]
    }
    

    and eventually

    {
       "Name" : "Root"
      ,"Branch1":
              [
                {"Name":"Leaf1", "Foo":"Bar"}
                {"Name":"Leaf2", "Foo":"Baz"} 
              ]
     ,"Branch2":
              [
                "Branch3":[
                            {"Name":"Leaf3", "Foo":"Quux"}
                          ]
              ]
    }
    
  • Brian Rogers
    Brian Rogers over 9 years
    This doesn't answer the question at all-- he is trying to serialize, not deserialize.
  • K. Alan Bates
    K. Alan Bates over 9 years
    "One problem with the "desired" JSON you specified in your question is that it is not entirely valid." You are very correct in that and I'm glad I made that mistake in writing it by hand. That is the desired JSON output I was going for (being composite data and all) but your observation is very helpful. I have mock json data that I crafted directly in my angular controller that I'm pushing down into the service layer with this task. For the time being, I can reasonably assume that there will only ever be 3 composite levels of the tree with a 4th level containing leaf nodes: (continued)
  • K. Alan Bates
    K. Alan Bates over 9 years
    root->composition->collection->items is the most complex this data gets, at least for the time being. I was going with a tree because the nesting level varies based on the area of the composition. One branch terminates at the third level, another terminates at 2, and several terminate at 4 but I don't have a set DataContract that would allow me to create concrete templates. I figured that a composite was the best starting point for managing this complexity
  • Brian Rogers
    Brian Rogers over 9 years
    In case it helps, I added the other converter I came up with. The output for this one is closer to the JSON you wanted, but again, the representation for a branch is inconsistent: it is either an array or an object depending on whether the children are all leaves or not. On the plus side, the JSON is more compact.