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.