Coding Stephan

Use System.Text.Json in Azure Functions

In 2019 Microsoft has released some new build-in API’s to (de/)serialize JSON. All the API’s in the System.Text.Json namespace should allow you to work with JavaScript Object Notation files without the need for Newtonsoft.JSON, the longtime go-to library to handle JSON. In this post I’ll explorer how to use the new api’s right from Azure Functions C#.

Newtonsoft.JSON

Newtonsoft.Json is a library developed by James Newton-King back in 2007. And as said, I was the number one library to handle JSON files from any .net application from 2007 all the way to at least 2020. I’m not saying it’s not the number one library any more, I don’t have any insights in the current usage compared to the alternative build by Microsoft itself.

I’ve used Newtonsoft.Json in several projects and for a long time it was included in many .net templates for web applications and API’s. It’s super customizable and almost all edge cases are possible or supported or a pain to remove support for.

System.Text.Json

Back in 2019 Microsoft wrote a blogpost that they created their own JSON api’s. The main reasons why they felt the need to create a new api were:

  • Their own main templates (for ASP.NET) depended on an external library, and since Microsoft is a commercial company that might make things complicated when you want to support your customers. I don’t know the exact reasons, this is just what I read between the lines.
  • They spend a lot of effort working on performance and in the race to as much requests per seconds the Newtonsoft.Json library might hold them back. They claim their new library increased the json part somewhere between 1.3 and 5 times.

Now in 2023 they replaced Newtonsoft.Json in all dotnet core templates with these built-in version in the System.Text.Json namespace, except in Azure Functions.

Json in Azure Functions

This template (new Azure Function -> HttpTrigger) uses Newtonsoft.Json to parse the body and to return a json string to the client.

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace MyFunctions
{
    public static class DemoFuntion
    {
        [FunctionName(nameof(DemoFuntion))]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            string name = req.Query["name"];

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            // Newtonsoft.Json
            dynamic data = JsonConvert.DeserializeObject(requestBody);
            name = name ?? data?.name;

            string responseMessage = string.IsNullOrEmpty(name)
                ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
                : $"Hello, {name}. This HTTP triggered function executed successfully.";
            
            // The OkObjectResult converts any object to a json string using Newtonsoft.Json
            return new OkObjectResult(responseMessage);
        }
    }
}

Return data using System.Text.Json

To write json to the client using the new System.Text.Json api’s you only have to replace a single line with two others.

// Replace this line
return new OkObjectResult(responseMessage);

// with
var data = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(responseMessage);
return new FileContentResult(data, "application/json");

This codes does not generate a json string, that then have to be converted to bytes before being send to the client. It converts the object to a byte array that is then send to the client with an FileContentResult which happens to accept a byte array and a content type.

Read the body using System.Text.Json

For reading the body from the HttpRequest, I’ve made this little extension method.

using Microsoft.AspNetCore.Http;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace MyFunctions
{
    public static class HttpRequestExtensions
    {
        internal static async Task<T> ParseJsonBodyAsync<T>(this HttpRequest request, CancellationToken cancellationToken = default)
        {
            var readResult = await request.BodyReader.ReadAsync(cancellationToken);
            return readResult.Buffer.ReadFromSequence<T>();
        }

        private static T ReadFromSequence<T>(this ReadOnlySequence<byte> sequence)
        {
            var reader = new Utf8JsonReader(sequence);
            return JsonSerializer.Deserialize<T>(ref reader, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!;
        }
    }
}

Use this extension as follows:

// You'll need a class, don't think System.Text.Json does support dynamic (that is one of those features that they did not implement)
public class PersonDto {
  public string? Name { get; set; }
}

// And then inside your function you call:
var person = await req.ParseJsonBodyAsync<PersonDto>();

Why do you want to use System.Text.Json?

I prefer to keep the used library the same for both sides of the conversation. They have different default settings for naming, case-sensitivity and enum handling. Since System.Text.Json seems the clear winner in speed that is what we want to be using. We really like Azure Functions to quickly build performant API’s, but because of the “don’t mix libraries”-policy we need our api’s to use System.Text.Json as well. This also allows use to share models (with the correct attributes) on both sides and saves us time in development.

You can check out the differences between the two libraries here, and I’ll post a comparison, performance wise between the two libraries for both reading and writing soon.

Conclusion

Dotnet moved to their own APIs for reading and writing JSON data, I was curious what it would take to also do this in Azure Functions. And it seems easy enough.

You cannot just remove Newtonsoft.Json from your Azure Functions app since it’s used in a lot of bindings as well. These steps at least allow you to use the more performant apis in HttpRequest. This also happens to be the place where most communication with external systems happens.

Data that will stay in Azure Functions, like you putting a message on the queue and handling those queue messages in another function, they still use Newtonsoft.Json, but if it keeps using the defaults, there should not be a miscommunication between the two.