3

In my database, I have a table with lots of columns and one of them contains a JSON string (I have no control over this). Something like this:

Name  Age  ExtraData
----  ---  ------------------
Bob    31  {c1: "1", c2: "2"}   <-- string with JSON

My Web API endpoint must return XML or JSON depending on the Accept headers in the request. Something like this:

JSON:

{
  "Name": "Bob",
  "Age": 31,
  "ExtraData": {
    "c1": 1,
    "c2": 2
  }
}

XML:

<person>
  <Name>Bob</Name>
  <Age>31</Age>
  <ExtraData>
    <c1>1</c1>
    <c2>2</c2>
  </ExtraData>
</person>

To do this, I have created a class in C# like this:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Object ExtraData { get; set; }
}

When parsing the data from database, I will fill in the ExtraData like this:

personInstance.ExtraData = JsonConvert.DeserializeObject(personTableRow.ExtraData);

When the Web API returns JSON, all works as expected.

When the Web API returns XML, it gives an Exception:

The 'ObjectContent`1' type failed to serialize the response body for content type 'application/xml; charset=utf-8'.

The inner exception, is something like this (it is not in English):

Newtonsoft.Json.Linq.JToken has a circular reference and that is not supported. (O tipo 'Newtonsoft.Json.Linq.JToken' é um contrato de dados de coleção recursiva que não é suportado. Considere modificar a definição da coleção 'Newtonsoft.Json.Linq.JToken' para remover referências a si mesma.)

Is there a way to parse the JSON data to an object without a circular reference?

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
bruno.almeida
  • 2,746
  • 1
  • 24
  • 32

1 Answers1

5

You have run into a limitation of the XmlSerializer. When deserializing a JSON object (which is an unordered set of name/value pairs surrounded by braces) to a c# object, Json.NET creates an object of type JObject, and unfortunately XmlSerializer does not know how to serialize a JObject. In particular it falls into an infinite recursion trying to serialize the children of JToken.Parent. Thus, you need to convert the underlying object ExtraData to a type XmlSerializer can handle.

However, it's not obvious what type to use, since:

What you can do is to take advantage of the [XmlAnyElement] functionality to create a surrogate property that converts your object ExtraData from and to an XElement using Json.NET's XmlNodeConverter:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    [XmlIgnore]
    [JsonProperty]
    public object ExtraData { get; set; }

    [XmlAnyElement("ExtraData")]
    [JsonIgnore]
    public XElement ExtraDataXml
    {
        get
        {
            return JsonExtensions.SerializeExtraDataXElement("ExtraData", ExtraData);
        }
        set
        {
            ExtraData = JsonExtensions.DeserializeExtraDataXElement("ExtraData", value);
        }
    }
}

public static class JsonExtensions
{
    public static XElement SerializeExtraDataXElement(string name, object extraData)
    {
        if (extraData == null)
            return null;
        var token = JToken.FromObject(extraData);
        if (token is JValue)
        {
            return new XElement(name, (string)token);
        }
        else if (token is JArray)
        {
            return new JObject(new JProperty(name, token)).ToXElement(false, name, true);
        }
        else
        {
            return token.ToXElement(false, name, true);
        }
    }

    public static object DeserializeExtraDataXElement(string name, XElement element)
    {
        object extraData;
        if (element == null)
            extraData = null;
        else
        {
            extraData = element.ToJToken(true, name, true);
            if (extraData is JObject)
            {
                var obj = (JObject)extraData;
                if (obj.Count == 1 && obj.Properties().First().Name == name)
                    extraData = obj.Properties().First().Value;
            }
            if (extraData is JValue)
            {
                extraData = ((JValue)extraData).Value;
            }
        }
        return extraData;
    }

    public static XElement ToXElement(this JToken obj, bool omitRootObject, string deserializeRootElementName, bool writeArrayAttribute)
    {
        if (obj == null)
            return null;
        using (var reader = obj.CreateReader())
        {
            var converter = new Newtonsoft.Json.Converters.XmlNodeConverter() { OmitRootObject = omitRootObject, DeserializeRootElementName = deserializeRootElementName, WriteArrayAttribute = writeArrayAttribute };
            var jsonSerializer = JsonSerializer.CreateDefault(new JsonSerializerSettings { Converters = { converter } });
            return jsonSerializer.Deserialize<XElement>(reader);
        }
    }

    public static JToken ToJToken(this XElement xElement, bool omitRootObject, string deserializeRootElementName, bool writeArrayAttribute)
    {
        // Convert to Linq to XML JObject
        var settings = new JsonSerializerSettings { Converters = { new XmlNodeConverter { OmitRootObject = omitRootObject, DeserializeRootElementName = deserializeRootElementName, WriteArrayAttribute = writeArrayAttribute } } };
        var root = JToken.FromObject(xElement, JsonSerializer.CreateDefault(settings));
        return root;
    }
}

Using the class above, I can deserialize your JSON and serialize to XML with the following result:

<Person>
  <Name>Bob</Name>
  <Age>31</Age>
  <ExtraData>
    <c1>1</c1>
    <c2>2</c2>
  </ExtraData>
</Person>

Note that there are inconsistencies between JSON and XML that cause problems:

  • JSON primitive values are "lightly" typed (as string, number, Boolean or null) whereas XML text is completely untyped. Thus numeric values (and dates) in the JSON get round-tripped to XML as strings.

  • XML has no concept of an array. Thus JSON whose root container is an array requires a synthetic root element to be added during serialization. This adds some code smell during the conversion process.

  • XML must have a single root element while valid JSON can consist of a primitive value, such as a string. Again a synthetic root element is required during conversion.

Lightly tested prototype fiddle here, where I demonstrate that the code works for ExtraData that is a JSON object, an array of strings, a single string, and a null value.

Community
  • 1
  • 1
dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    Great answer. Thanks. Work as expected. I almost got the same result, only using JsonConvert.DeserializeXmlNode(json), and then return the XmlNode, but i always had an extra level in xml. What i was missing was the XmlAnyElementAttribute. – bruno.almeida Dec 06 '16 at 10:52