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); }