diff --git a/Src/Newtonsoft.Json.Tests/Converters/XmlNodeConverterTest.cs b/Src/Newtonsoft.Json.Tests/Converters/XmlNodeConverterTest.cs
index 6c49c1ed..3dfe29d2 100644
--- a/Src/Newtonsoft.Json.Tests/Converters/XmlNodeConverterTest.cs
+++ b/Src/Newtonsoft.Json.Tests/Converters/XmlNodeConverterTest.cs
@@ -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 = @"
- Alan
- http://www.google.com
+ Alan
+ http://www.google.com
- Louis
- http://www.yahoo.com
+ Louis
+ http://www.yahoo.com
";
@@ -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(@"assets/images/resized/0001/1070/11070v1-max-150x150.jpgassets/images/resized/0001/1070/11070v1-max-150x150.jpgassets/images/resized/0001/1070/11070v1-max-250x250.jpgassets/images/resized/0001/1070/11070v1-max-250x250.jpg", newDoc.InnerXml);
+ string xml = IndentXml(newDoc.InnerXml);
+
+ Assert.AreEqual(@"
+
+ 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
+
+", IndentXml(newDoc.InnerXml));
#if !NET20
XDocument newXDoc = JsonConvert.DeserializeXNode(json, "myRoot");
- Assert.AreEqual(@"assets/images/resized/0001/1070/11070v1-max-150x150.jpgassets/images/resized/0001/1070/11070v1-max-150x150.jpgassets/images/resized/0001/1070/11070v1-max-250x250.jpgassets/images/resized/0001/1070/11070v1-max-250x250.jpg", newXDoc.ToString(SaveOptions.DisableFormatting));
+ Assert.AreEqual(@"
+
+ 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
+
+", 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(@"
+
+ 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
+
+", IndentXml(newDoc.InnerXml));
+
+#if !NET20
+ XDocument newXDoc = JsonConvert.DeserializeXNode(json, "myRoot", true);
+
+ Console.WriteLine(IndentXml(newXDoc.ToString(SaveOptions.DisableFormatting)));
+
+ Assert.AreEqual(@"
+
+ 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
+
+", 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(@"
+ Apple
+ 2008-12-28T00:00:00Z
+ 3.99
+ Small
+", 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 Products { get; set; }
+ }
+
+ [Test]
+ public void ComplexSingleItemArrayPropertySerialization()
+ {
+ TestComplexArrayClass o = new TestComplexArrayClass
+ {
+ Name = "Hi",
+ Products = new List
+ {
+ new Product { Name = "First" }
+ }
+ };
+
+ string output = JsonConvert.SerializeObject(o, new IsoDateTimeConverter());
+
+ XmlDocument xmlProduct = JsonConvert.DeserializeXmlNode(output, "test", true);
+
+ Assert.AreEqual(@"
+ Hi
+
+ First
+ 2000-01-01T00:00:00Z
+ 0
+
+
+", 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 = @"
+ Hi
+ Hi
+
+ First
+ 2000-01-01T00:00:00Z
+ 0
+
+
+";
+
+ 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
\ No newline at end of file
diff --git a/Src/Newtonsoft.Json/Converters/XmlNodeConverter.cs b/Src/Newtonsoft.Json/Converters/XmlNodeConverter.cs
index d9b92926..8c388dff 100644
--- a/Src/Newtonsoft.Json/Converters/XmlNodeConverter.cs
+++ b/Src/Newtonsoft.Json/Converters/XmlNodeConverter.cs
@@ -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
/// The name of the deserialize root element.
public string DeserializeRootElementName { get; set; }
+ ///
+ /// 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.
+ ///
+ /// true if the array attibute is written to the XML; otherwise, false.
+ public bool WriteArrayAttribute { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to write the root JSON object.
+ ///
+ /// true if the JSON root object is omitted; otherwise, false.
+ public bool OmitRootObject { get; set; }
+
#region Writing
///
/// 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> nodesGroupedByName = new Dictionary>();
@@ -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().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 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().Single(n => n.LocalName == propertyName);
+ AddJsonArrayAttribute(arrayElement, document);
}
}
else
diff --git a/Src/Newtonsoft.Json/JsonConvert.cs b/Src/Newtonsoft.Json/JsonConvert.cs
index 38b56cda..f3a09788 100644
--- a/Src/Newtonsoft.Json/JsonConvert.cs
+++ b/Src/Newtonsoft.Json/JsonConvert.cs
@@ -785,6 +785,20 @@ namespace Newtonsoft.Json
return SerializeObject(node, formatting, converter);
}
+ ///
+ /// Serializes the XML node to a JSON string.
+ ///
+ /// The node to serialize.
+ /// Indicates how the output is formatted.
+ /// Omits writing the root object.
+ /// A JSON string of the XmlNode.
+ public static string SerializeXmlNode(XmlNode node, Formatting formatting, bool omitRootObject)
+ {
+ XmlNodeConverter converter = new XmlNodeConverter { OmitRootObject = omitRootObject };
+
+ return SerializeObject(node, formatting, converter);
+ }
+
///
/// Deserializes the XmlNode from a JSON string.
///
@@ -802,9 +816,25 @@ namespace Newtonsoft.Json
/// The name of the root element to append when deserializing.
/// The deserialized XmlNode
public static XmlDocument DeserializeXmlNode(string value, string deserializeRootElementName)
+ {
+ return DeserializeXmlNode(value, deserializeRootElementName, false);
+ }
+
+ ///
+ /// Deserializes the XmlNode from a JSON string nested in a root elment.
+ ///
+ /// The JSON string.
+ /// The name of the root element to append when deserializing.
+ ///
+ /// 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.
+ ///
+ /// The deserialized XmlNode
+ 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
/// A JSON string of the XNode.
public static string SerializeXNode(XObject node, Formatting formatting)
{
- XmlNodeConverter converter = new XmlNodeConverter();
+ return SerializeXNode(node, formatting, false);
+ }
+
+ ///
+ /// Serializes the to a JSON string.
+ ///
+ /// The node to serialize.
+ /// Indicates how the output is formatted.
+ /// Omits writing the root object.
+ /// A JSON string of the XNode.
+ 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
/// The name of the root element to append when deserializing.
/// The deserialized XNode
public static XDocument DeserializeXNode(string value, string deserializeRootElementName)
+ {
+ return DeserializeXNode(value, deserializeRootElementName, false);
+ }
+
+ ///
+ /// Deserializes the from a JSON string nested in a root elment.
+ ///
+ /// The JSON string.
+ /// The name of the root element to append when deserializing.
+ ///
+ /// 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.
+ ///
+ /// The deserialized XNode
+ 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);
}