-Added WriteArrayAttribute to XmlNodeConverter to include an attribute to help round tripping JSON and XML

-Added OmitRootObject to XmlNodeConverter to skip writing the root JSON object when converting XML
This commit is contained in:
JamesNK
2010-12-27 04:00:43 +00:00
parent 2c92f69c74
commit 87a2bbf831
3 changed files with 415 additions and 52 deletions
@@ -25,7 +25,9 @@
#if !SILVERLIGHT
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Tests.Serialization;
using Newtonsoft.Json.Tests.TestObjects;
using NUnit.Framework;
using Newtonsoft.Json;
using System.IO;
@@ -423,12 +425,12 @@ namespace Newtonsoft.Json.Tests.Converters
string xml = @"<?xml version=""1.0"" standalone=""no""?>
<root>
<person id=""1"">
<name>Alan</name>
<url>http://www.google.com</url>
<name>Alan</name>
<url>http://www.google.com</url>
</person>
<person id=""2"">
<name>Louis</name>
<url>http://www.yahoo.com</url>
<name>Louis</name>
<url>http://www.yahoo.com</url>
</person>
</root>";
@@ -651,19 +653,110 @@ namespace Newtonsoft.Json.Tests.Converters
[
""assets/images/resized/0001/1070/11070v1-max-250x250.jpg"",
""assets/images/resized/0001/1070/11070v1-max-250x250.jpg""
],
[
""assets/images/resized/0001/1070/11070v1-max-250x250.jpg""
]
]
}";
XmlDocument newDoc = JsonConvert.DeserializeXmlNode(json, "myRoot");
Assert.AreEqual(@"<myRoot><available_sizes><available_sizes>assets/images/resized/0001/1070/11070v1-max-150x150.jpg</available_sizes><available_sizes>assets/images/resized/0001/1070/11070v1-max-150x150.jpg</available_sizes></available_sizes><available_sizes><available_sizes>assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes><available_sizes>assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes></available_sizes></myRoot>", newDoc.InnerXml);
string xml = IndentXml(newDoc.InnerXml);
Assert.AreEqual(@"<myRoot>
<available_sizes>
<available_sizes>assets/images/resized/0001/1070/11070v1-max-150x150.jpg</available_sizes>
<available_sizes>assets/images/resized/0001/1070/11070v1-max-150x150.jpg</available_sizes>
</available_sizes>
<available_sizes>
<available_sizes>assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes>
<available_sizes>assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes>
</available_sizes>
<available_sizes>
<available_sizes>assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes>
</available_sizes>
</myRoot>", IndentXml(newDoc.InnerXml));
#if !NET20
XDocument newXDoc = JsonConvert.DeserializeXNode(json, "myRoot");
Assert.AreEqual(@"<myRoot><available_sizes><available_sizes>assets/images/resized/0001/1070/11070v1-max-150x150.jpg</available_sizes><available_sizes>assets/images/resized/0001/1070/11070v1-max-150x150.jpg</available_sizes></available_sizes><available_sizes><available_sizes>assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes><available_sizes>assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes></available_sizes></myRoot>", newXDoc.ToString(SaveOptions.DisableFormatting));
Assert.AreEqual(@"<myRoot>
<available_sizes>
<available_sizes>assets/images/resized/0001/1070/11070v1-max-150x150.jpg</available_sizes>
<available_sizes>assets/images/resized/0001/1070/11070v1-max-150x150.jpg</available_sizes>
</available_sizes>
<available_sizes>
<available_sizes>assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes>
<available_sizes>assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes>
</available_sizes>
<available_sizes>
<available_sizes>assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes>
</available_sizes>
</myRoot>", IndentXml(newXDoc.ToString(SaveOptions.DisableFormatting)));
#endif
string newJson = JsonConvert.SerializeXmlNode(newDoc, Formatting.Indented);
Console.WriteLine(newJson);
}
[Test]
public void RoundTripNestedArrays()
{
string json = @"{
""available_sizes"": [
[
""assets/images/resized/0001/1070/11070v1-max-150x150.jpg"",
""assets/images/resized/0001/1070/11070v1-max-150x150.jpg""
],
[
""assets/images/resized/0001/1070/11070v1-max-250x250.jpg"",
""assets/images/resized/0001/1070/11070v1-max-250x250.jpg""
],
[
""assets/images/resized/0001/1070/11070v1-max-250x250.jpg""
]
]
}";
XmlDocument newDoc = JsonConvert.DeserializeXmlNode(json, "myRoot", true);
Assert.AreEqual(@"<myRoot>
<available_sizes json:Array=""true"" xmlns:json=""http://james.newtonking.com/projects/json"">
<available_sizes>assets/images/resized/0001/1070/11070v1-max-150x150.jpg</available_sizes>
<available_sizes>assets/images/resized/0001/1070/11070v1-max-150x150.jpg</available_sizes>
</available_sizes>
<available_sizes json:Array=""true"" xmlns:json=""http://james.newtonking.com/projects/json"">
<available_sizes>assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes>
<available_sizes>assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes>
</available_sizes>
<available_sizes json:Array=""true"" xmlns:json=""http://james.newtonking.com/projects/json"">
<available_sizes json:Array=""true"">assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes>
</available_sizes>
</myRoot>", IndentXml(newDoc.InnerXml));
#if !NET20
XDocument newXDoc = JsonConvert.DeserializeXNode(json, "myRoot", true);
Console.WriteLine(IndentXml(newXDoc.ToString(SaveOptions.DisableFormatting)));
Assert.AreEqual(@"<myRoot>
<available_sizes json:Array=""true"" xmlns:json=""http://james.newtonking.com/projects/json"">
<available_sizes>assets/images/resized/0001/1070/11070v1-max-150x150.jpg</available_sizes>
<available_sizes>assets/images/resized/0001/1070/11070v1-max-150x150.jpg</available_sizes>
</available_sizes>
<available_sizes json:Array=""true"" xmlns:json=""http://james.newtonking.com/projects/json"">
<available_sizes>assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes>
<available_sizes>assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes>
</available_sizes>
<available_sizes json:Array=""true"" xmlns:json=""http://james.newtonking.com/projects/json"">
<available_sizes json:Array=""true"">assets/images/resized/0001/1070/11070v1-max-250x250.jpg</available_sizes>
</available_sizes>
</myRoot>", IndentXml(newXDoc.ToString(SaveOptions.DisableFormatting)));
#endif
string newJson = JsonConvert.SerializeXmlNode(newDoc, Formatting.Indented, true);
Assert.AreEqual(json, newJson);
}
[Test]
@@ -992,6 +1085,140 @@ namespace Newtonsoft.Json.Tests.Converters
DeserializeXmlNode(json);
}
[Test]
public void SingleItemArrayPropertySerialization()
{
Product product = new Product();
product.Name = "Apple";
product.ExpiryDate = new DateTime(2008, 12, 28, 0, 0, 0, DateTimeKind.Utc);
product.Price = 3.99M;
product.Sizes = new string[] { "Small" };
string output = JsonConvert.SerializeObject(product, new IsoDateTimeConverter());
XmlDocument xmlProduct = JsonConvert.DeserializeXmlNode(output, "product", true);
Assert.AreEqual(@"<product>
<Name>Apple</Name>
<ExpiryDate>2008-12-28T00:00:00Z</ExpiryDate>
<Price>3.99</Price>
<Sizes json:Array=""true"" xmlns:json=""http://james.newtonking.com/projects/json"">Small</Sizes>
</product>", IndentXml(xmlProduct.InnerXml));
string output2 = JsonConvert.SerializeXmlNode(xmlProduct.DocumentElement, Formatting.Indented);
Assert.AreEqual(@"{
""product"": {
""Name"": ""Apple"",
""ExpiryDate"": ""2008-12-28T00:00:00Z"",
""Price"": ""3.99"",
""Sizes"": [
""Small""
]
}
}", output2);
}
public class TestComplexArrayClass
{
public string Name { get; set; }
public IList<Product> Products { get; set; }
}
[Test]
public void ComplexSingleItemArrayPropertySerialization()
{
TestComplexArrayClass o = new TestComplexArrayClass
{
Name = "Hi",
Products = new List<Product>
{
new Product { Name = "First" }
}
};
string output = JsonConvert.SerializeObject(o, new IsoDateTimeConverter());
XmlDocument xmlProduct = JsonConvert.DeserializeXmlNode(output, "test", true);
Assert.AreEqual(@"<test>
<Name>Hi</Name>
<Products json:Array=""true"" xmlns:json=""http://james.newtonking.com/projects/json"">
<Name>First</Name>
<ExpiryDate>2000-01-01T00:00:00Z</ExpiryDate>
<Price>0</Price>
<Sizes />
</Products>
</test>", IndentXml(xmlProduct.InnerXml));
string output2 = JsonConvert.SerializeXmlNode(xmlProduct.DocumentElement, Formatting.Indented, true);
Assert.AreEqual(@"{
""Name"": ""Hi"",
""Products"": [
{
""Name"": ""First"",
""ExpiryDate"": ""2000-01-01T00:00:00Z"",
""Price"": ""0"",
""Sizes"": null
}
]
}", output2);
}
private string IndentXml(string xml)
{
XmlReader reader = XmlReader.Create(new StringReader(xml));
StringWriter sw = new StringWriter();
XmlWriter writer = XmlWriter.Create(sw, new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true });
while (reader.Read())
{
writer.WriteNode(reader, false);
}
writer.Flush();
return sw.ToString();
}
[Test]
public void OmitRootObject()
{
string xml = @"<test>
<Name>Hi</Name>
<Name>Hi</Name>
<Products json:Array=""true"" xmlns:json=""http://james.newtonking.com/projects/json"">
<Name>First</Name>
<ExpiryDate>2000-01-01T00:00:00Z</ExpiryDate>
<Price>0</Price>
<Sizes />
</Products>
</test>";
XmlDocument d = new XmlDocument();
d.LoadXml(xml);
string output = JsonConvert.SerializeXmlNode(d, Formatting.Indented, true);
Assert.AreEqual(@"{
""Name"": [
""Hi"",
""Hi""
],
""Products"": [
{
""Name"": ""First"",
""ExpiryDate"": ""2000-01-01T00:00:00Z"",
""Price"": ""0"",
""Sizes"": null
}
]
}", output);
}
}
}
#endif
@@ -136,6 +136,11 @@ namespace Newtonsoft.Json.Converters
_element.SetAttributeNode((XmlAttribute) xmlAttributeWrapper.WrappedNode);
}
public string GetPrefixOfNamespace(string namespaceURI)
{
return _element.GetPrefixOfNamespace(namespaceURI);
}
}
internal class XmlDeclarationWrapper : XmlNodeWrapper, IXmlDeclaration
@@ -294,6 +299,7 @@ namespace Newtonsoft.Json.Converters
internal interface IXmlElement : IXmlNode
{
void SetAttributeNode(IXmlNode attribute);
string GetPrefixOfNamespace(string namespaceURI);
}
internal interface IXmlNode
@@ -726,6 +732,11 @@ namespace Newtonsoft.Json.Converters
{
get { return Element.Name.NamespaceName; }
}
public string GetPrefixOfNamespace(string namespaceURI)
{
return Element.GetPrefixOfNamespace(namespaceURI);
}
}
#endif
#endregion
@@ -749,6 +760,19 @@ namespace Newtonsoft.Json.Converters
/// <value>The name of the deserialize root element.</value>
public string DeserializeRootElementName { get; set; }
/// <summary>
/// Gets or sets a flag to indicate whether to write the Json.NET array attribute.
/// This attribute helps preserve arrays when converting the written XML back to JSON.
/// </summary>
/// <value><c>true</c> if the array attibute is written to the XML; otherwise, <c>false</c>.</value>
public bool WriteArrayAttribute { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to write the root JSON object.
/// </summary>
/// <value><c>true</c> if the JSON root object is omitted; otherwise, <c>false</c>.</value>
public bool OmitRootObject { get; set; }
#region Writing
/// <summary>
/// Writes the JSON representation of the object.
@@ -763,9 +787,13 @@ namespace Newtonsoft.Json.Converters
XmlNamespaceManager manager = new XmlNamespaceManager(new NameTable());
PushParentNamespaces(node, manager);
writer.WriteStartObject();
SerializeNode(writer, node, manager, true);
writer.WriteEndObject();
if (!OmitRootObject)
writer.WriteStartObject();
SerializeNode(writer, node, manager, !OmitRootObject);
if (!OmitRootObject)
writer.WriteEndObject();
}
private IXmlNode WrapXml(object value)
@@ -856,7 +884,16 @@ namespace Newtonsoft.Json.Converters
}
}
private void SerializeGroupedNodes(JsonWriter writer, IXmlNode node, XmlNamespaceManager manager)
private bool IsArray(IXmlNode node)
{
IXmlNode jsonArrayAttribute = (node.Attributes != null)
? node.Attributes.SingleOrDefault(a => a.LocalName == "Array" && a.NamespaceURI == JsonNamespaceUri)
: null;
return (jsonArrayAttribute != null && XmlConvert.ToBoolean(jsonArrayAttribute.Value));
}
private void SerializeGroupedNodes(JsonWriter writer, IXmlNode node, XmlNamespaceManager manager, bool writePropertyName)
{
// group nodes together by name
Dictionary<string, List<IXmlNode>> nodesGroupedByName = new Dictionary<string, List<IXmlNode>>();
@@ -885,14 +922,7 @@ namespace Newtonsoft.Json.Converters
if (groupedNodes.Count == 1)
{
IXmlNode singleNode = groupedNodes[0];
IXmlNode jsonArrayAttribute = (singleNode.Attributes != null)
? singleNode.Attributes.SingleOrDefault(a => a.LocalName == "Array" && a.NamespaceURI == JsonNamespaceUri)
: null;
if (jsonArrayAttribute != null)
writeArray = XmlConvert.ToBoolean(jsonArrayAttribute.Value);
else
writeArray = false;
writeArray = IsArray(groupedNodes[0]);
}
else
{
@@ -901,12 +931,15 @@ namespace Newtonsoft.Json.Converters
if (!writeArray)
{
SerializeNode(writer, groupedNodes[0], manager, true);
SerializeNode(writer, groupedNodes[0], manager, writePropertyName);
}
else
{
string elementNames = nodeNameGroup.Key;
writer.WritePropertyName(elementNames);
if (writePropertyName)
writer.WritePropertyName(elementNames);
writer.WriteStartArray();
for (int i = 0; i < groupedNodes.Count; i++)
@@ -925,47 +958,54 @@ namespace Newtonsoft.Json.Converters
{
case XmlNodeType.Document:
case XmlNodeType.DocumentFragment:
SerializeGroupedNodes(writer, node, manager);
SerializeGroupedNodes(writer, node, manager, writePropertyName);
break;
case XmlNodeType.Element:
foreach (IXmlNode attribute in node.Attributes)
if (IsArray(node) && node.ChildNodes.All(n => n.LocalName == node.LocalName))
{
if (attribute.NamespaceURI == "http://www.w3.org/2000/xmlns/")
{
string prefix = (attribute.LocalName != "xmlns")
? attribute.LocalName
: string.Empty;
manager.AddNamespace(prefix, attribute.Value);
}
}
if (writePropertyName)
writer.WritePropertyName(GetPropertyName(node, manager));
if (ValueAttributes(node.Attributes).Count() == 0 && node.ChildNodes.Count == 1
&& node.ChildNodes[0].NodeType == XmlNodeType.Text)
{
// write elements with a single text child as a name value pair
writer.WriteValue(node.ChildNodes[0].Value);
}
else if (node.ChildNodes.Count == 0 && CollectionUtils.IsNullOrEmpty(node.Attributes))
{
// empty element
writer.WriteNull();
SerializeGroupedNodes(writer, node, manager, false);
}
else
{
writer.WriteStartObject();
for (int i = 0; i < node.Attributes.Count; i++)
foreach (IXmlNode attribute in node.Attributes)
{
SerializeNode(writer, node.Attributes[i], manager, true);
if (attribute.NamespaceURI == "http://www.w3.org/2000/xmlns/")
{
string prefix = (attribute.LocalName != "xmlns")
? attribute.LocalName
: string.Empty;
manager.AddNamespace(prefix, attribute.Value);
}
}
SerializeGroupedNodes(writer, node, manager);
if (writePropertyName)
writer.WritePropertyName(GetPropertyName(node, manager));
writer.WriteEndObject();
if (ValueAttributes(node.Attributes).Count() == 0 && node.ChildNodes.Count == 1
&& node.ChildNodes[0].NodeType == XmlNodeType.Text)
{
// write elements with a single text child as a name value pair
writer.WriteValue(node.ChildNodes[0].Value);
}
else if (node.ChildNodes.Count == 0 && CollectionUtils.IsNullOrEmpty(node.Attributes))
{
// empty element
writer.WriteNull();
}
else
{
writer.WriteStartObject();
for (int i = 0; i < node.Attributes.Count; i++)
{
SerializeNode(writer, node.Attributes[i], manager, true);
}
SerializeGroupedNodes(writer, node, manager, true);
writer.WriteEndObject();
}
}
break;
@@ -1116,6 +1156,7 @@ namespace Newtonsoft.Json.Converters
{
if (reader.TokenType == JsonToken.StartArray)
{
// handle nested arrays
ReadArrayElements(reader, document, propertyName, currentNode, manager);
return;
}
@@ -1200,10 +1241,39 @@ namespace Newtonsoft.Json.Converters
currentNode.AppendChild(nestedArrayElement);
int count = 0;
while (reader.Read() && reader.TokenType != JsonToken.EndArray)
{
DeserializeValue(reader, document, manager, propertyName, nestedArrayElement);
count++;
}
if (WriteArrayAttribute)
{
AddJsonArrayAttribute(nestedArrayElement, document);
}
if (count == 1 && WriteArrayAttribute)
{
IXmlElement arrayElement = nestedArrayElement.ChildNodes.CastValid<IXmlElement>().Single(n => n.LocalName == propertyName);
AddJsonArrayAttribute(arrayElement, document);
}
}
private void AddJsonArrayAttribute(IXmlElement element, IXmlDocument document)
{
element.SetAttributeNode(document.CreateAttribute("json:Array", JsonNamespaceUri, "true"));
#if !NET20
// linq to xml doesn't automatically include prefixes via the namespace manager
if (element is XElementWrapper)
{
if (element.GetPrefixOfNamespace(JsonNamespaceUri) == null)
{
element.SetAttributeNode(document.CreateAttribute("xmlns:json", "http://www.w3.org/2000/xmlns/", JsonNamespaceUri));
}
}
#endif
}
private Dictionary<string, string> ReadAttributeElements(JsonReader reader, XmlNamespaceManager manager)
@@ -1355,9 +1425,17 @@ namespace Newtonsoft.Json.Converters
if (reader.TokenType == JsonToken.StartArray)
{
int count = 0;
while (reader.Read() && reader.TokenType != JsonToken.EndArray)
{
DeserializeValue(reader, document, manager, propertyName, currentNode);
count++;
}
if (count == 1 && WriteArrayAttribute)
{
IXmlElement arrayElement = currentNode.ChildNodes.CastValid<IXmlElement>().Single(n => n.LocalName == propertyName);
AddJsonArrayAttribute(arrayElement, document);
}
}
else
+59 -1
View File
@@ -785,6 +785,20 @@ namespace Newtonsoft.Json
return SerializeObject(node, formatting, converter);
}
/// <summary>
/// Serializes the XML node to a JSON string.
/// </summary>
/// <param name="node">The node to serialize.</param>
/// <param name="formatting">Indicates how the output is formatted.</param>
/// <param name="omitRootObject">Omits writing the root object.</param>
/// <returns>A JSON string of the XmlNode.</returns>
public static string SerializeXmlNode(XmlNode node, Formatting formatting, bool omitRootObject)
{
XmlNodeConverter converter = new XmlNodeConverter { OmitRootObject = omitRootObject };
return SerializeObject(node, formatting, converter);
}
/// <summary>
/// Deserializes the XmlNode from a JSON string.
/// </summary>
@@ -802,9 +816,25 @@ namespace Newtonsoft.Json
/// <param name="deserializeRootElementName">The name of the root element to append when deserializing.</param>
/// <returns>The deserialized XmlNode</returns>
public static XmlDocument DeserializeXmlNode(string value, string deserializeRootElementName)
{
return DeserializeXmlNode(value, deserializeRootElementName, false);
}
/// <summary>
/// Deserializes the XmlNode from a JSON string nested in a root elment.
/// </summary>
/// <param name="value">The JSON string.</param>
/// <param name="deserializeRootElementName">The name of the root element to append when deserializing.</param>
/// <param name="writeArrayAttribute">
/// A flag to indicate whether to write the Json.NET array attribute.
/// This attribute helps preserve arrays when converting the written XML back to JSON.
/// </param>
/// <returns>The deserialized XmlNode</returns>
public static XmlDocument DeserializeXmlNode(string value, string deserializeRootElementName, bool writeArrayAttribute)
{
XmlNodeConverter converter = new XmlNodeConverter();
converter.DeserializeRootElementName = deserializeRootElementName;
converter.WriteArrayAttribute = writeArrayAttribute;
return (XmlDocument)DeserializeObject(value, typeof(XmlDocument), converter);
}
@@ -829,7 +859,19 @@ namespace Newtonsoft.Json
/// <returns>A JSON string of the XNode.</returns>
public static string SerializeXNode(XObject node, Formatting formatting)
{
XmlNodeConverter converter = new XmlNodeConverter();
return SerializeXNode(node, formatting, false);
}
/// <summary>
/// Serializes the <see cref="XNode"/> to a JSON string.
/// </summary>
/// <param name="node">The node to serialize.</param>
/// <param name="formatting">Indicates how the output is formatted.</param>
/// <param name="omitRootObject">Omits writing the root object.</param>
/// <returns>A JSON string of the XNode.</returns>
public static string SerializeXNode(XObject node, Formatting formatting, bool omitRootObject)
{
XmlNodeConverter converter = new XmlNodeConverter { OmitRootObject = omitRootObject };
return SerializeObject(node, formatting, converter);
}
@@ -851,9 +893,25 @@ namespace Newtonsoft.Json
/// <param name="deserializeRootElementName">The name of the root element to append when deserializing.</param>
/// <returns>The deserialized XNode</returns>
public static XDocument DeserializeXNode(string value, string deserializeRootElementName)
{
return DeserializeXNode(value, deserializeRootElementName, false);
}
/// <summary>
/// Deserializes the <see cref="XNode"/> from a JSON string nested in a root elment.
/// </summary>
/// <param name="value">The JSON string.</param>
/// <param name="deserializeRootElementName">The name of the root element to append when deserializing.</param>
/// <param name="writeArrayAttribute">
/// A flag to indicate whether to write the Json.NET array attribute.
/// This attribute helps preserve arrays when converting the written XML back to JSON.
/// </param>
/// <returns>The deserialized XNode</returns>
public static XDocument DeserializeXNode(string value, string deserializeRootElementName, bool writeArrayAttribute)
{
XmlNodeConverter converter = new XmlNodeConverter();
converter.DeserializeRootElementName = deserializeRootElementName;
converter.WriteArrayAttribute = writeArrayAttribute;
return (XDocument)DeserializeObject(value, typeof(XDocument), converter);
}