Your first thought is probably "but why?" and that’s fair. So let me explain. I’m a heavy user of Microsoft Flow and Azure Logic Apps, and both of those offerings have really good build in support for JSON, but not for XML. In fact you can’t even really parse XML in to objects you can then reference in either of these. So, enter my desire to want to convert XML to JSON so I can pass it to the Parse JSON
step of these and use it later on. Some endpoints I want to query only give me back XML. So here I am. That said, doing this with Azure Functions wasn’t as straightforward as I had hoped so here I am sharing with you, dear reader. Let’s get started. Create an HTTP Trigger Azure Function project in Visual Studio Function
access rights (vs Anonymous) for this one because it’d be an easy Function to abuse should anybody discover the URL. Once you’ve got that, here’s the content to use to create two functions, one to convert JSON to XML and another to convert XML to JSON:
[FunctionName("ConvertToJson")]
public static IActionResult RunToJson([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, TraceWriter log)
{
if (req.ContentType.IndexOf(@"/xml", 0, System.StringComparison.OrdinalIgnoreCase) == -1)
{
return new BadRequestObjectResult(@"Content-Type header must be an XML content type");
}
XmlDocument doc = new XmlDocument();
doc.Load(req.Body);
return new OkObjectResult(doc);
}
[FunctionName("ConvertToXml")]
public static async Task<HttpResponseMessage> RunToXmlAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, TraceWriter log)
{
if (req.ContentType.IndexOf(@"/json", 0, System.StringComparison.OrdinalIgnoreCase) == -1)
{
return new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
{
Content = new StringContent(@"Content-Type header must be a JSON content type")
};
}
var json = await req.ReadAsStringAsync();
XmlDocument doc = JsonConvert.DeserializeXmlNode(json);
StringBuilder output = new StringBuilder();
using (var sw = new StringWriter(output))
doc.WriteTo(new XmlTextWriter(sw));
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(output.ToString(), Encoding.Default, @"application/xml"),
};
}
Dissecting ConvertToJson
As a set up for later, let’s check out how simple it was to get XML -> JSON: Let’s review some of the gymnastics and how I arrived at the XML -> JSON code shown above:
if (req.ContentType.IndexOf(@"/xml", 0, System.StringComparison.OrdinalIgnoreCase) == -1)
{
return new BadRequestObjectResult(@"Content-Type header must be an XML content type");
}
XmlDocument doc = new XmlDocument();
doc.Load(req.Body);
return new OkObjectResult(doc);
Here was my test request in Postman:
POST /api/ConvertToJson HTTP/1.1
Host: localhost:7071
Content-Type: application/xml
Cache-Control: no-cache
Postman-Token: a5dc4ca4-b6dd-4193-b590-d15982219da7
<root>
<this att="x">
<underthis>val</underthis>
</this>
<that>
<withval>x</withval>
<bigval>
<![CDATA[
something something
]]>
</bigval>
</that>
</root>
And here’s what you get back:
Content-Type →application/json; charset=utf-8
Date →Fri, 25 May 2018 18:01:16 GMT
Server →Kestrel
Transfer-Encoding →chunked
{
"root": {
"this": {
"@att": "x",
"underthis": "val"
},
"that": {
"withval": "x",
"bigval": {
"#cdata-section": "\n\t\t\tsomething something\n\t\t\t"
}
}
}
}
Because Functions automatically takes any object
given to OkObjectResult
and runs it through JSON deserialization, simply giving it the XmlDocument
resulting from LoadXml
gives us exactly what we want! But this comes with some baggage…
Dissecting ConvertToXml
This one was even more bizarre.
if (req.ContentType.IndexOf(@"/json", 0, System.StringComparison.OrdinalIgnoreCase) == -1)
{
return new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
{
Content = new StringContent(@"Content-Type header must be a JSON content type")
};
}
var json = await req.ReadAsStringAsync();
XmlDocument doc = JsonConvert.DeserializeXmlNode(json);
StringBuilder output = new StringBuilder();
using (var sw = new StringWriter(output))
doc.WriteTo(new XmlTextWriter(sw));
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(output.ToString(), Encoding.Default, @"application/xml"),
};
The general approach to getting JSON in to XML is to take it and just serialize it to an XmlNode
using Newtonsoft.Json’s constructs. No biggie there, I did that. For starters, here’s the Postman request we’re going to be sending to ConvertToXml
:
POST /api/ConvertToXml HTTP/1.1
Host: localhost:7071
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 7e55a73f-1d94-46b2-b93f-e7d1297c0c30
{
"root": {
"this": {
"@att": "x",
"underthis": "val"
},
"that": {
"withval": "x",
"bigval": {
"#cdata-section": "\n\t\t\tsomething something\n\t\t\t"
}
}
}
}
So now let’s investigate why we can’t just take the resulting XmlDocument
object and write it out to the OkObjectResult
. The first thing we have to change to experiment here is the return value of ConvertToXml
. You’ll notice it’s set to HttpResponseMessage
which is the type used, typically, in a v1 Azure Function, not a v2. More on that later, but change it back to IActionResult
so the signature now looks like:
public static async Task<IActionResult> RunToXmlAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, TraceWriter log)
Now, change the body to this:
if (req.ContentType.IndexOf(@"/json", 0, System.StringComparison.OrdinalIgnoreCase) == -1)
{
return new BadRequestObjectResult(@"Content-Type header must be an JSON content type");
}
var json = await req.ReadAsStringAsync();
XmlDocument doc = JsonConvert.DeserializeXmlNode(json);
return new OkObjectResult(doc);
and give ‘er a go, only to see JSON come out:
Content-Type →application/json; charset=utf-8
Date →Fri, 25 May 2018 17:44:59 GMT
Server →Kestrel
Transfer-Encoding →chunked
{
"root": {
"this": {
"@att": "x",
"underthis": "val"
},
"that": {
"withval": "x",
"bigval": {
"#cdata-section": "\n\t\t\tsomething something\n\t\t\t"
}
}
}
}
At this point, I tried adding [Produces(@"application/xml")]
to the signature of my Function but it had no effect whatsoever. Functions must not respect those ASP.Net Core attributes, unfortunately (yet?). Let’s try forcing the output to be XML using the MediaTypeFormatters
collection again:
return new OkObjectResult(doc) { ContentTypes = new Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection { @"application/xml" } };
Nada. This time we get HTTP 406 NOT ACCEPTABLE
as the output of our Function. OK. Let’s take the XML document, write it to a string, and spit that out.
XmlDocument doc = JsonConvert.DeserializeXmlNode(json);
StringBuilder sb = new StringBuilder();
using (var sw = new StringWriter(sb))
doc.WriteTo(new XmlTextWriter(sw));
return new OkObjectResult(sb.ToString());
Gives us:
Content-Type →text/plain; charset=utf-8
Date →Fri, 25 May 2018 17:51:16 GMT
Server →Kestrel
Transfer-Encoding →chunked
<root><this att="x"><underthis>val</underthis></this><that><withval>x</withval><bigval><![CDATA[
something something
]]></bigval></that></root>
Close! But I really want that Content-Type
header to be accurate, darnit! Let’s add our MediaTypeFormatter
back in:
return new OkObjectResult(sb.ToString()) { ContentTypes = new Microsoft.AspNetCore.Mvc.Formatters.MediaTypeCollection { @"application/xml" } };
Giving us…..
Content-Type →application/xml; charset=utf-8
Date →Fri, 25 May 2018 17:52:58 GMT
Server →Kestrel
Transfer-Encoding →chunked
<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/"><root><this att="x"><underthis>val</underthis></this><that><withval>x</withval><bigval><![CDATA[
something something
]]></bigval></that></root></string>
DAG NABBIT! (not even close to the actual words I was uttering at this point) Clearly ASP.Net Core and/or Functions is doing something automatic under the covers that I’m just not able to control. I knew ASP.Net MVC handles this kind of stuff beautifully; maybe .Net Core is just pushing us all to a JSON-only world? ¯(ツ)/¯ As a last-ditch effort I converted this Function to use the ASP.Net MVC constructs for response messages. Starting with the signature:
public static async Task<HttpResponseMessage> RunToXmlAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequest req, TraceWriter log)
Then each response I was sending back:
return new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
{
Content = new StringContent(@"Content-Type header must be a JSON content type")
};
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(output.ToString(), Encoding.Default, @"application/xml"),
};
and wouldn’t you know…
Content-Length →149
Content-Type →application/xml; charset=utf-8
Date →Fri, 25 May 2018 17:57:08 GMT
Server →Kestrel
<root>
<this att="x">
<underthis>val</underthis>
</this>
<that>
<withval>x</withval>
<bigval>
<![CDATA[
something something
]]>
</bigval>
</that>
</root>
TADA! We’ve got a body in XML, and we’ve got the header set to indicate it. Perfect! Having one function using the ASP.Net Core constructs (ConvertToJson
) and one using ASP.Net MVC constructs doesn’t seem to hurt matters at all, either. Enjoy! And hey, if you’re an ASP.Net Core expert and see what I might be doing wrong in trying to get the desired headers & output for my Functions, please let me know in the comments!
Update
I posted my experience as a bug/feedback issue over on the Azure Functions Host repo and got a response from the (awesome) product team. They directed me to, instead, use one of two options:
Accept
header to trigger content negotiation between my Function and the Client (eg: sendAccept: application/xml
and the Function should return XML as bothcontent-type
header and the body payload)- Use
ContentResult
instead ofOkObjectResult
and set the appropriate properties.
Let’s walk through each of these.
For the Accept
header, I changed my code for the XML part of the Function to look like this:
if (req.ContentType.IndexOf(@"/json", 0, System.StringComparison.OrdinalIgnoreCase) == -1)
{
return new BadRequestObjectResult(@"Content-Type header must be a JSON content type");
}
var json = await req.ReadAsStringAsync();
XmlDocument doc = JsonConvert.DeserializeXmlNode(json);
return new OkObjectResult(doc);
Then my Postman requests becomes:
POST /api/ConvertToXml HTTP/1.1
Host: localhost:7071
Content-Type: application/json
Accept: application/xml
Cache-Control: no-cache
Postman-Token: a37ba05f-ae2a-4541-b44f-9d23edf44616
{
"root": {
"this": {
"@att": "x",
"underthis": "val"
},
"that": {
"withval": "x",
"bigval": {
"#cdata-section": "\n\t\t\tsomething something\n\t\t\t"
}
}
}
}
But unfortunately I didn’t see what I expected:
{
"root": {
"this": {
"@att": "x",
"underthis": "val"
},
"that": {
"withval": "x",
"bigval": {
"#cdata-section": "\n\t\t\tsomething something\n\t\t\t"
}
}
}
}
If I change the function to, instead of returning the XmlDocument
object to returning the string we built up previously:
XmlDocument doc = JsonConvert.DeserializeXmlNode(json);
StringBuilder output = new StringBuilder();
using (var sw = new StringWriter(output))
doc.WriteTo(new XmlTextWriter(sw));
return new OkObjectResult(output.ToString());
I get back:
<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/"><root><this att="x"><underthis>val</underthis></this><that><withval>x</withval><bigval><![CDATA[
something something
]]></bigval></that></root></string>
which looks familiar, but is still incorrect.
Let’s check out this ContentResult
response. Our code now becomes:
XmlDocument doc = JsonConvert.DeserializeXmlNode(json);
StringBuilder output = new StringBuilder();
using (var sw = new StringWriter(output))
doc.WriteTo(new XmlTextWriter(sw));
return new ContentResult
{
Content = output.ToString(),
ContentType = @"application/xml",
StatusCode = StatusCodes.Status200OK
};
and we get back:
<root>
<this att="x">
<underthis>val</underthis>
</this>
<that>
<withval>x</withval>
<bigval>
<![CDATA[
something something
]]>
</bigval>
</that>
</root>
Yay! We’re now fully within the .Net Core arena and returning back XML. It’s also worth noting that with the ContentResult
approach the Accept
header on the request is inconsequential.