3

I am trying to create an XML document that closely conforms to a C# object graph and its JSON representation, but am having difficulty with the list representation in the XML. Given this graph

public class X
{
    public List<A> Aa { get; set; }
}

public class A
{
    public int B;
    public bool C;
}

I took the JSON from the above, and tried converting it a couple of ways:

var json = @"{""Aa"":[{""B"":186,""C"":true},{""B"":9,""C"":false},{""B"":182,""C"":true}]}";
var xml = JsonConvert.DeserializeXNode(json, typeof(T).Name, false);
var xml2 = JsonToXml(json);

This produced the following for xml (no Aa "container node"):

<X>
  <Aa><B>186</B><C>true</C></Aa>
  <Aa><B>9</B><C>false</C></Aa>
  <Aa><B>182</B><C>true</C></Aa>
</X>

And for xml2 (has "container" node, but some extra noise):

<root type="object">
  <Aa type="array">
    <item type="object">
      <B type="number">186</B>
      <C type="boolean">true</C>
    </item>
    <item type="object">
      <B type="number">9</B>
      <C type="boolean">false</C>
    </item>
    <item type="object">
      <B type="number">182</B>
      <C type="boolean">true</C>
    </item>
  </Aa>
</root>

The method used to produce the value for xml2 comes from a different approach using the .NET Framework:

    XDocument JsonToXml(string jsonString)
    {
        using (var stream = new MemoryStream(Encoding.ASCII.GetBytes(jsonString)))
        {
            var quotas = new XmlDictionaryReaderQuotas();
            return XDocument.Load(JsonReaderWriterFactory.CreateJsonReader(stream, quotas));
        }
    }

What I want to produce is

<X>
  <Aa>
    <A><B>186</B><C>true</C></A>
    <A><B>9</B><C>false</C></A>
    <A><B>182</B><C>true</C></A>
  </Aa>
</X>

I have tried changing the writeArrayAttribute parameter of DeserializeXDocument to true, but that doesn't work either. The documentation for converting between JSON and XML does not help.

How can I produce the compact version that contains the items in a parent Aa node? Is this going to require some custom deserializer?

The original JSON was created via

var json = JsonConvert.SerializeObject(new X { etc }, Formatting.None, settings);
Community
  • 1
  • 1
Kit
  • 20,354
  • 4
  • 60
  • 103
  • 1
    Wouldn't the JSON look like this then? `{"Aa": {"B": [30,2,31]}}` or something similar? That XML output you are getting looks correct to me as you requested an array of `Aa` items, not `B` items. You could try to build out POCOs that serialize to the XML you want and then serialize to JSON to see what it did so you can compare the two. – TyCobb Aug 02 '16 at 17:08
  • I updated it with a slightly less contrived example. – Kit Aug 02 '16 at 19:57
  • `{"Aa": {"A": [{"B":186, "C": true},{"B":9, "C": false},{"B":182, "C": true}]}}` should produce the desired XML you want. The array's key will be used as the parent element for each object/value inside of the array. Your original XML looks goofy probably because you're missing the XmlSerialization attributes that tell it how it needs to serialize. `[XmlElement("A")]` on your list probably would have removed the `item` tag or could have done both `[XmlArray("Aa")][XmlArrayItem("A")]` to produce parent `Aa` and children `A`. – TyCobb Aug 02 '16 at 20:21
  • What's goofy to me is that the JSON created by that last line of code shown in my question doesn't match the JSON in your comment. I'm thinking that's the root of the problem, because, yes, your JSON makes sense. – Kit Aug 02 '16 at 21:06

1 Answers1

2

The Problem.

Your difficulty arises because there are two common ways to serialize a collection to XML, and Json.NET only supports automatic JSON-to-XML conversion for one of them.

Specifically, when serializing a c# collection to XML (with, say, XmlSerializer), the collection can be serialized either with, or without, an outer container element. The former looks like the following:

<X>
  <Aa>
    <A>
      <B>186</B>
      <C>true</C>
    </A>
    <A>
      <B>9</B>
      <C>false</C>
    </A>
  </Aa>
</X>

While the latter looks like:

<X>
  <Aa>
    <B>186</B>
    <C>true</C>
  </Aa>
  <Aa>
    <B>9</B>
    <C>false</C>
  </Aa>
</X>

When Json.NET converts a JSON array to XML elements, it uses the second format for the array, since the JSON only contains one property name while the two-level XML format requires both inner and outer element names. I.e. in your JSON:

{"Aa":[{"B":186,"C":true},{"B":9,"C":false}]}

Only the name "Aa" appears. The name "A" never does, so DeserializeXNode() cannot know to insert it. This makes the second format the straightforward choice for canonical conversion, whereas you require the first.

The Solution.

To generate a two-level XML collection from a JSON array, you'll need to either insert synthetic JSON objects before conversion, or synthetic XML elements afterwards. For the former, this can be done by parsing the JSON string to an intermediate JToken, and modifying it as follows:

var jObject = JObject.Parse(json);

jObject.SelectTokens("Aa").WrapWithObjects("A");

var finalXml = jObject.ToXElement(typeof(X).Name, false);

Using the extension methods:

public static class JsonExtensions
{
    public static void WrapWithObjects(this IEnumerable<JToken> values, string name)
    {
        foreach (var value in values.ToList())
        {
            var newParent = new JObject();
            if (value.Parent != null)
                value.Replace(newParent);
            newParent[name] = value;
        }
    }

    public static XElement ToXElement(this JObject obj, string deserializeRootElementName = null, bool writeArrayAttribute = false)
    {
        if (obj == null)
            return null;
        using (var reader = obj.CreateReader())
            return JsonExtensions.DeserializeXElement(reader, deserializeRootElementName, writeArrayAttribute);
    }

    static XElement DeserializeXElement(JsonReader reader, string deserializeRootElementName, bool writeArrayAttribute)
    {
        var converter = new Newtonsoft.Json.Converters.XmlNodeConverter() { DeserializeRootElementName = deserializeRootElementName, WriteArrayAttribute = writeArrayAttribute };
        var jsonSerializer = JsonSerializer.CreateDefault(new JsonSerializerSettings { Converters = new JsonConverter[] { converter } });
        return jsonSerializer.Deserialize<XElement>(reader);
    }
}

Alternatively, you could tell XmlSerializer to (de)serialize the Aa list without a container element by marking it with [XmlElement]:

public class X
{
    [XmlElement]
    public List<A> Aa { get; set; }
}

Now the xml generated by JsonConvert.DeserializeXNode will be deserializable directly.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Perfect, thanks! I took your solution and checked the `JObject.Type` so That I can apply this throughout the graph. – Kit Aug 02 '16 at 22:38