Creating an XML <-> JSON Converter in Azure Functions

By | May 25, 2018

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 I recommend sticking with 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/">&lt;root&gt;&lt;this att="x"&gt;&lt;underthis&gt;val&lt;/underthis&gt;&lt;/this&gt;&lt;that&gt;&lt;withval&gt;x&lt;/withval&gt;&lt;bigval&gt;&lt;![CDATA[
            something something
            ]]&gt;&lt;/bigval&gt;&lt;/that&gt;&lt;/root&gt;</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: send Accept: application/xml and the Function should return XML as both content-type header and the body payload)
  • Use ContentResult instead of OkObjectResult 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/">&lt;root&gt;&lt;this att="x"&gt;&lt;underthis&gt;val&lt;/underthis&gt;&lt;/this&gt;&lt;that&gt;&lt;withval&gt;x&lt;/withval&gt;&lt;bigval&gt;&lt;![CDATA[
            something something
            ]]&gt;&lt;/bigval&gt;&lt;/that&gt;&lt;/root&gt;</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.