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
.
TAMILAN
Very nice explanation