I had a requirement where I needed to send a message to a web API that only accepted messages as JSON.
BizTalk2013 provides a WCF-WebHttp adaptor for sending sending XML messages to a REST endpoint but not in JSON format. There are rumours this will be remedied in BizTalk2013R2, unfortunately I required a solution now.
So in the meantime I will use the the Json.NET component to convert the XML message to JSON. I also found this blog from http://blog.quicklearn.com/2013/09/06/biztalk-server-2013-support-for-restful-services-part-45/ to convert XML messages to JSON using the Json.net in a custom pipeline component which I ended up using with some mods.
When using this pipeline remember to set the Outbound HTTP Headers as shown below:
This all worked a treat until during the unit testing phase I created a sample XML document with only one repeating element.This caused the following error from the Web API.
Can not deserialize instance of java.util.ArrayList out of VALUE_STRING token at [Source: java.io.StringReader@6b270743; line: 1, column: 152] (through reference chain: model.ExtContact["names"])
As it turned out, the Web API required all unbound elements to be sent as an array which should have looked like this “names”: [“Mahindra Morar”] but instead was being sent as “names”:”Mahindra Morar”
After reading the documentation on the Json.Net component there is an option to always force a json array for an element. This is done by adding the following attribute json:Array=”true” on the unbound element.
Here is a code snippet from the documentation that shows how it is done.
Now I though I could just simply import a schema with the element and attribute which produced the following schema output.
<ns0:Root xmlns:ns0=”http://BizTalk_Server_Project1.People”>
<ns1:Name Array=”true” xmlns:ns1=”http://james.newtonking.com/projects/json”>Tom Lee</ns1:Name>
</ns0:Root>
However the Json.net component did not like having the namespace declared inline with the unbound element when it was generated by BizTalk. The work around for this was to add the attribute to the elements in the custom pipeline component, use the XMLTranslatorStream to remove the inline namespaces or debug the source code. I ended up using the first option as I already had the XML document loaded in a XMLDoument type. To keep the solution a bit more flexible, I exposed a property on the pipeline to accept a list of comma separated xpath elements that required to be outputted as json arrays.
The Execute method in the code from http://blog.quicklearn.com/2013/09/06/biztalk-server-2013-support-for-restful-services-part-45/ was modified as shown below.
public Microsoft.BizTalk.Message.Interop.IBaseMessage Execute(Microsoft.BizTalk.Component.Interop.IPipelineContext pc, Microsoft.BizTalk.Message.Interop.IBaseMessage inmsg) { #region Handle CORS Requests // Detect of the incoming message is an HTTP CORS request // http://www.w3.org/TR/cors/ object httpMethod = null; httpMethod = inmsg.Context.Read(HTTP_METHOD_PROPNAME, WCF_PROPERTIES_NS); if (httpMethod != null && (httpMethod as string) == OPTIONS_METHOD) { // Remove the message body before returning var emptyOutputStream = new VirtualStream(); inmsg.BodyPart.Data = emptyOutputStream; return inmsg; } #endregion // Make message seekable if (!inmsg.BodyPart.Data.CanSeek) { var originalStream = inmsg.BodyPart.Data; Stream seekableStream = new ReadOnlySeekableStream(originalStream); inmsg.BodyPart.Data = seekableStream; pc.ResourceTracker.AddResource(originalStream); } // Here again we are loading the entire document into memory // this is still a bad plan, and shouldn't be done in production // if you expect larger message sizes XmlDocument xmlDoc = new XmlDocument(); xmlDoc.Load(inmsg.BodyPart.Data); //Get the array list of the elements required to be outputted as json arrays from the exposed pipeline property. string[] elementList = arrayXpathElementList.Split(','); //add the namespace required to force an element. xmlDoc.DocumentElement.SetAttribute("xmlns:json", "http://james.newtonking.com/projects/json"); if (xmlDoc.FirstChild.LocalName == "xml") xmlDoc.RemoveChild(xmlDoc.FirstChild); for (int indexElement = 0; indexElement < elementList.Length; indexElement++) { XmlNodeList dataNodes = xmlDoc.SelectNodes(elementList[indexElement]); foreach (XmlNode node in dataNodes) { //Add the attribute required on the element to force the creation of an json array. string ns = node.GetNamespaceOfPrefix("json"); XmlNode attr = xmlDoc.CreateNode(XmlNodeType.Attribute, "Array", ns); attr.Value = "true"; node.Attributes.SetNamedItem(attr); } } // Remove any root-level attributes added in the process of creating the XML // (Think xmlns attributes that have no meaning in JSON) string jsonString = JsonConvert.SerializeXmlNode(xmlDoc, Newtonsoft.Json.Formatting.Indented, true); #region Handle JSONP Request // Here we are detecting if there has been any value promoted to the jsonp callback property // which will contain the name of the function that should be passed the JSON data returned // by the service. object jsonpCallback = inmsg.Context.Read(JSONP_CALLBACK_PROPNAME, JSON_SCHEMAS_NS); string jsonpCallbackName = (jsonpCallback ?? (object)string.Empty) as string; if (!string.IsNullOrWhiteSpace(jsonpCallbackName)) jsonString = string.Format("{0}({1});", jsonpCallbackName, jsonString); #endregion var outputStream = new VirtualStream(new MemoryStream(Encoding.UTF8.GetBytes(jsonString))); inmsg.BodyPart.Data = outputStream; return inmsg; } #endregion }
After making the changes above, the component is now able to covert a unbound element with only one value to a json array correctly.
Enjoy.