Background


Creating Azure Functions using C# and .NET can feel quite familiar to those versed in creating ASP.NET Core Web APIs. Despite the similarities, one key difference lies in the process of JSON serialization, which can present unexpected challenges. In this post, I'll explore the two most common issues developers encounter with JSON in Azure Functions and provide practical solutions to overcome them.

Please be noted: This blog post only talks about Azure Function V4 with .NET 6.0 in process worker. Things may change in the future version of Azure Function.

Customize Json Property Name


Problem

Define a class, with JsonPropertyName attribute, like this:

class Product
{
    [JsonPropertyName("displayName")]
    public string Name { get; set; }
    public decimal Price { get; set; }
}

What I want is to output the Name property in Json as "displayName"

Now, guess what's the result of this function?

[FunctionName("Function1")]
public static IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");

    var products = new List<Product>
    {
        new() { Name = "Apple", Price = 1.99m },
        new() { Name = "Banana", Price = 2.99m },
        new() { Name = "Cherry", Price = 3.99m }
    };

    return new OkObjectResult(products);
}

The JsonPropertyName does not work.

[
    {
        "name": "Apple",
        "price": 1.99
    },
    {
        "name": "Banana",
        "price": 2.99
    },
    {
        "name": "Cherry",
        "price": 3.99
    }
]

Root cause

The underlying issue stems from Azure Functions' reliance on a different serialization engine than one might expect. Instead of utilizing System.Text.Json, which is common in modern .NET applications, Azure Functions default to the venerable Newtonsoft.Json package. This package isn't explicitly listed in your project file; rather, it's included as a dependency of the Microsoft.NET.Sdk.Functions package.

Although the coding experience in C# for Azure Functions closely mirrors that of ASP.NET Core, and both may run on .NET 6.0, assumptions about consistency in JSON serialization can lead to confusion. Contrary to ASP.NET Core 6.0, which defaults to System.Text.Json for serialization, Azure Functions diverge in this aspect. Despite using the same OkObjectResult class from ASP.NET Core's framework, the serialization process under the hood is handled differently due to the reliance on Newtonsoft.Json

#region Assembly Microsoft.AspNetCore.Mvc.Core, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
// location unknown
// Decompiled with ICSharpCode.Decompiler 8.1.1.7464
#endregion

using Microsoft.AspNetCore.Mvc.Infrastructure;

namespace Microsoft.AspNetCore.Mvc;

//
// Summary:
//     An Microsoft.AspNetCore.Mvc.ObjectResult that when executed performs content
//     negotiation, formats the entity body, and will produce a Microsoft.AspNetCore.Http.StatusCodes.Status200OK
//     response if negotiation and formatting succeed.
[DefaultStatusCode(200)]
public class OkObjectResult : ObjectResult
{
    private const int DefaultStatusCode = 200;

    //
    // Summary:
    //     Initializes a new instance of the Microsoft.AspNetCore.Mvc.OkObjectResult class.
    //
    //
    // Parameters:
    //   value:
    //     The content to format into the entity body.
    public OkObjectResult(object value)
        : base(value)
    {
        base.StatusCode = 200;
    }
}

Fix

Use JsonProperty from Newtonsoft.Json

class Product
{
    [JsonProperty("displayName")]
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Now, the response is what I desired.

[
    {
        "displayName": "Apple",
        "price": 1.99
    },
    {
        "displayName": "Banana",
        "price": 2.99
    },
    {
        "displayName": "Cherry",
        "price": 3.99
    }
]

Model Binding


Problem

This is a real bug in my blog's email sending function. This code intends to serialize req.Payload to a string to be inserted in to Azure Storage Queue.

[FunctionName("Enqueue")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] EnqueueRequest req,
    ILogger log)
{
    log.LogInformation($"C# HTTP trigger function `Enqueue` processed a request. OriginAspNetRequestId={req.OriginAspNetRequestId}");

    var conn = Environment.GetEnvironmentVariable("moongladestorage");
    var queue = new QueueClient(conn, "moongladeemailqueue");

    var en = new EmailNotification
    {
        DistributionList = string.Join(';', req.Receipts),
        MessageType = req.Type,
        MessageBody = System.Text.Json.JsonSerializer.Serialize(req.Payload, MoongladeJsonSerializerOptions.Default),
    };

    await InsertMessageAsync(queue, en, log);

    return new AcceptedResult();
}

The EnqueueRequest class has a Payload property with object type.

public class EnqueueRequest
{
    public string Type { get; set; }
    public string[] Receipts { get; set; }
    public object Payload { get; set; }
    public string OriginAspNetRequestId { get; set; } = Guid.Empty.ToString();
}

But in runtime, the MessageBody never get the correct Json string. 

The values in the original req object are removed after serialization. 

It intends to be:

{
  "name": "Fubao",
  "price": 996.35
}

But it just become this:

"{\r\n  \"name\": [],\r\n  \"price\": []\r\n}"

Root cause

Again, this is because the model binding in Azure Function is also using Newtonsoft.Json instead of System.Text.Json like many will assume it to be. In this case, I use object type for Payload property. This will become Newtonsoft.Json.Linq.Object, which System.Text.Json.JsonSerializer has no idea how to serialize it correctly.

Fix

Change the code to use JsonConvert from Newtonsoft.Json

MessageBody = JsonConvert.SerializeObject(req.Payload)

Now it works fine

Conclusion


While C# Azure Functions and ASP.NET Core share many similarities in their development models, there's a significant difference in how they handle JSON serialization and deserialization. Azure Functions, by default, use Newtonsoft.Json for both serializing response objects and deserializing incoming request data during model binding. This is a critical point of consideration for developers who might be accustomed to System.Text.Json, which is the default in ASP.NET Core. Special attention is needed when working with Azure Functions to ensure that any custom serialization settings or attributes are compatible with Newtonsoft.Json, and developers should be cautious when attempting to use System.Text.Json in scenarios where Azure Functions implicitly expect Newtonsoft.Json.