2

I'm trying to convert a xml to json using Json.net (JsonConvert.SerializeXNode).

There is a problem when you try to convert between an xml and a json if you don't use some kind of schema (xsd), since you can't really identify the difference between an xml collection with a single element to a regular object.

Example:

<Drivers>
  <Driver>
    <Name>MyName</Name>
  </Driver>
</Drivers>

Will be converted into:

"Drivers":{ "Driver": { "Name": "MyName" } }

since nobody tells the serializer that Drivers is a collection with a single object and it thinks it's just a regular object.

Json.net has a work around for this using the json:Array='true' tagging.

Everything works great when you tag the arrays, but it creates an extra middle object (Driver):

"Drivers": [{"Driver":{"Name": "MyName"}}]

Now I understand why this node is created, but I'm trying to find a way to bypass that creating. I would like to get this result:

"Drivers": [{"Name": "MyName"}]

Does anyone have any idea how to do something like this?

Amir Popovich
  • 29,350
  • 9
  • 53
  • 99

3 Answers3

1

Json.NET will not automatically convert a collection serialized with an outer container element (e.g., one generated with [XmlArray] attached) to a single JSON array. Instead, you will need to manually flatten the unwanted level of nesting by pre-processing the XML with LINQ-to-XML or post-processing with LINQ-to-JSON.

Since you are already preprocessing your XML to add the json:Array='true' attribute, adding some additional preprocessing would seem to be most straightforward. First, introduce the following extension method:

public static class XNodeExtensions
{
    /// <summary>
    /// Flatten a two-level collection with an outer container element to a one-level collection 
    /// in preparation for conversion to JSON using Json.NET
    /// </summary>
    /// <param name="parents">The outer container elements.</param>
    /// <param name="childName">The inner element name.  If null, flatten all children.</param>
    /// <param name="newChildName">The new element name.  If null, use the parent name.</param>
    public static void FlattenCollection(this IEnumerable<XElement> parents, XName childName = null, XName newChildName = null)
    {
        if (parents == null)
            throw new ArgumentNullException();

        XNamespace json = @"http://james.newtonking.com/projects/json";
        XName isArray = json + "Array";

        foreach (var parent in parents.ToList())
        {
            if (parent.Parent == null)
                continue; // Removed or root
            foreach (var child in (childName == null ? parent.Elements() : parent.Elements(childName)).ToList())
            {
                child.Remove();
                child.Name = newChildName ?? parent.Name;
                child.Add(new XAttribute(isArray, true));
                parent.Parent.Add(child);
            }
            if (!parent.HasElements)
                parent.Remove();
        }
    }
}

You can now do:

var xnode = XDocument.Parse(xml);
xnode.Descendants("Drivers").FlattenCollection();
var json = JsonConvert.SerializeXNode(xnode, Formatting.Indented, true);

And get the JSON you want. Sample fiddle.

Note that <Drivers> cannot be the root element to do this, since an XML document must have exactly one root node.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    Thanks for your response. I thought about flattening the xml as my last plan and since I didn't find a better way to do it, I'm going to do it. About the implementation, I've implemented this a bit different to suit my case. Thanks for response. – Amir Popovich Jun 07 '16 at 08:14
0

Running the example below with the flag omitRootObject set to true will do it. If you turn it to false, you indeed get the upper Drivers node - that you are trying to get rid of.

    XmlDocument xmlDoc = new XmlDocument();
    xmlDoc.LoadXml(@"<Drivers>
                  <Driver>
                    <Name>MyName</Name>
                  </Driver>
                </Drivers>");


    var result = JsonConvert.SerializeXmlNode(xmlDoc.FirstChild, Newtonsoft.Json.Formatting.Indented, true);

    Console.WriteLine(result);

Output:

enter image description here

Set omitRootNode to false and you will get the root node you are trying to get rid of.

enter image description here

Veverke
  • 9,208
  • 4
  • 51
  • 95
  • First of all, Drivers is not my only node. I have a large xml and it's a part of it. Second of all, I need the response to look like `"Drivers": [{"Name": "MyName"}]`. Thanks for trying. – Amir Popovich Jun 06 '16 at 17:03
0

I've implemented the flattening a bit different. I've accepted dbc's answer since it does answer the question, but here's my implementation just in case someone will run into this in the future and need it:

public static class XNodeExtensions
{
    public static void FlattenCollection(this XElement parent)
    {
        if (parent == null || parent.Parent == null)
            throw new ArgumentNullException();

        if (parent.Elements().Count() == 0)
            return;

        XNamespace json = @"http://james.newtonking.com/projects/json";
        XName isArray = json + "Array";

        foreach (var child in parent.Elements().ToList())
        {
            child.Remove();
            child.Name = parent.Name;
            child.Add(new XAttribute(isArray, true));
            parent.Parent.Add(child);
        }

        parent.Remove();
    }
}
Amir Popovich
  • 29,350
  • 9
  • 53
  • 99