azure-functions-host icon indicating copy to clipboard operation
azure-functions-host copied to clipboard

Add Config to set output serializer to System.Text.Json

Open shibayan opened this issue 4 years ago • 17 comments

What problem would the feature you're requesting solve? Please describe.

Solves the problem that input serializer and output serializer are different. Function v3 uses System.Text.Json as input serializer and Newtonsoft.Json as output serializer. (#5299)

When sharing a Domain Model with an ASP.NET Core application, different serializers can cause compatibility issues.

Describe the solution you'd like

I need to change the output serializer to System.Text.Json.

Describe alternatives you've considered

There is a way to configure all applications to use Newtonsoft.Json. However, it does not match the current trend of .NET Core.

Additional context

https://github.com/Azure/azure-functions-host/blob/v3.x/src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs#L75

shibayan avatar Jan 12 '20 12:01 shibayan

Thanks @shibayan for opening the issue. Functions v3 uses Newtonsoft.Json for serializer at all times. Is there a place you saw System.Text.Json used for serializing?

Regardless of that, this sounds like a fair ask to use System.Text.Json. I am not sure the work that may be involved and if it's something we will be able to look at very soon.

Adding @fabiocav if he had any comment on this ask or priority.

cc: @brettsam (had a super brief chat on this offline)

ankitkumarr avatar Jan 13 '20 22:01 ankitkumarr

@ankitkumarr Thank you for replying. Yes. Changing the default output serializer to System.Text.Json also has compatibility issues, so I want it to be switchable in settings.

ASP.NET Core 3.0 (MVC / SignalR) uses System.Text.Json by default. Attributes such as JsonProperty and JsonIgnore are not compatible with each other, causing problems when sharing domain models.

shibayan avatar Jan 14 '20 01:01 shibayan

@fabiocav @ankitkumarr

I actually came here to see if there was an issue covering a move to use STJ when auto-deserializing POCOs as part of an HttpTrigger, etc. but haven't found something encompassing quite that just yet.

Would it be fair to write an issue that says we should remove usage of NJ in favor of STJ in v3? Folks that requires NJ for compatibility can then import the package (free of unification struggles, yay!) and continue on, but w/in Functions and as part of things that are auto serialized/deserialized we'd be using STJ.

If we don't have an issue like this I'm happy to capture one. I think it would quite heavily impact each first-party extension as well, but they could each get their own similar issue.

I'm also happy to help out w/ the effort if acceptable. Or perhaps this better fits as an issue for the Webjobs SDK repo?

brandonh-msft avatar May 05 '20 15:05 brandonh-msft

@brandonh-msft the serialization is not the main issue, the binding support is. A large number of apps use JObject as the binding target type. Removing that in the 3.0 timeframe wasn't viable, and removing in a current major would be a breaking change for those apps.

This is also the only reason why runtime unification exists, but in the vast majority of cases, this should not present a problem, and you still have the flexibility to reference different versions internally.

Do you have a concrete example of a problem you're dealing with because of that behavior?

fabiocav avatar May 05 '20 16:05 fabiocav

I am also currently running into the problem of needing to use a custom JsonConverter that's designed to work with System.Text.Json, but Azure Functions appears to enforce Newtonsoft with no way to override in Startup.Configure

pheuter avatar Aug 04 '20 19:08 pheuter

I'm having an issue with this as well (https://github.com/Azure/azure-functions-host/issues/5203#issuecomment-667859103).

I believe it is impossible to use System.Text.Json when

  • returning an OkObjectResult,
  • when using the built-in deserialization of trigger input

This is annoying because projects shared between an asp.net core project and an azure functions project will have to "live in 2 worlds".

Imagine you want to set [JsonConverter(typeof(JsonStringEnumConverter))] on a data model enum (using System.Text.Json) and you want to use that in Azure Functions, it's a no go

hansmbakker avatar Aug 06 '20 14:08 hansmbakker

Concerning "Function v3 uses System.Text.Json as input serializer and Newtonsoft.Json as output serializer." I have also observed this to be the case. This is confusing and IMO would have been much more logical if MS had stuck with one or the other.

ByteDev avatar Nov 05 '20 02:11 ByteDev

I just ran into the same case. Will this get fixed with .NET5 for Azure Functions?

stevo-knievo avatar Dec 30 '20 02:12 stevo-knievo

@stevo-knievo the model with the OOP worker for .NET 5 in functions is a bit different, but you have full control over the stack there.

fabiocav avatar Dec 30 '20 22:12 fabiocav

@fabiocav thanks for your reply! I'm looking forward to using .NET5 together with Azure Functions.

stevo-knievo avatar Dec 30 '20 22:12 stevo-knievo

I am having trouble with this as well. Internally using System.Text.Json, including custom converters and pocos, so far so good. When trying to return a JsonResult from a HttpTrigger I realized that it is using Newtonsoft (and thus wont take my JsonSerializerOptions)

is there any way I can work around this?

mtin avatar Jan 27 '21 18:01 mtin

Same here, having an object in the model and System.Text.Json transforms this to "ValueKind" objects but then when serialized via OkObjectResult gives me a corrupt Value. an easy sample is a bool object of false get transformed to a object : { "valueKind": 6 } Do I need to rewoke all this to NewtonSoft?

Sample Object Read from Cosmos via System.Text.Json [ { "Name": "Enabled", "Value": false, "Type": "bool", "DefaultValue": true, "AllowedValues": [] }, { "Name": "Severity", "Value": "Low", "Type": "enum", "DefaultValue": "Medium", "AllowedValues": [ "Low", "Medium", "Warning", "Critical" ] } ] Ends up like this when using OkObjectResult or JsonResult as return IActionResult it ends up like this, note the valueKind. And that breaks the consumer. [ { "name": "Enabled", "value": { "valueKind": 6 }, "type": "bool", "defaultValue": true, "allowedValues": [] }, { "name": "Severity", "value": { "valueKind": 3 }, "type": "enum", "defaultValue": "Medium", "allowedValues": [ "Low", "Medium", "Warning", "Critical" ] } ]

MLogdberg avatar Feb 06 '21 22:02 MLogdberg

This doesn't help people with existing Azure Functions, but the Azure Functions for .net 5 isolated runtime allows you to configure the JSON serialization options now. So for anyone starting new azure functions or have the budget and energy to upgrade existing ones, there's now first-class support for configuring the serializer exactly how you like it.

I think System.Text.Json is now the default, but if you want Newtonsoft.Json it looks like there's a way to opt-in to and configure either. I found this example.

Program.cs

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.Core.Serialization;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace Configuration
{
    public class Program
    {
        public static void Main()
        {
            var host = new HostBuilder()
                .ConfigureFunctionsWorkerDefaults(workerApplication =>
                {
                    // Use any of the extension methods in WorkerConfigurationExtensions.
                })
                .Build();

            host.Run();
        }
    }

    internal static class WorkerConfigurationExtensions
    {
        /// <summary>
        /// Calling ConfigureFunctionsWorkerDefaults() configures the Functions Worker to use System.Text.Json for all JSON
        /// serialization and sets JsonSerializerOptions.PropertyNameCaseInsensitive = true;
        /// This method uses DI to modify the JsonSerializerOptions. Call /api/HttpFunction to see the changes.
        /// </summary>
        public static IFunctionsWorkerApplicationBuilder ConfigureSystemTextJson(this IFunctionsWorkerApplicationBuilder builder)
        {
            builder.Services.Configure<JsonSerializerOptions>(jsonSerializerOptions =>
            {
                jsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
                jsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
                jsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;

                // override the default value
                jsonSerializerOptions.PropertyNameCaseInsensitive = false;
            });

            return builder;
        }

        /// <summary>
        /// The functions worker uses the Azure SDK's ObjectSerializer to abstract away all JSON serialization. This allows you to
        /// swap out the default System.Text.Json implementation for the Newtonsoft.Json implementation.
        /// To do so, add the Microsoft.Azure.Core.NewtonsoftJson nuget package and then update the WorkerOptions.Serializer property.
        /// This method updates the Serializer to use Newtonsoft.Json. Call /api/HttpFunction to see the changes.
        /// </summary>
        public static IFunctionsWorkerApplicationBuilder UseNewtonsoftJson(this IFunctionsWorkerApplicationBuilder builder)
        {
            builder.Services.Configure<WorkerOptions>(workerOptions =>
            {
                var settings = NewtonsoftJsonObjectSerializer.CreateJsonSerializerSettings();
                settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
                settings.NullValueHandling = NullValueHandling.Ignore;

                workerOptions.Serializer = new NewtonsoftJsonObjectSerializer(settings);
            });

            return builder;
        }
    }
}

tyson-benson avatar Jul 30 '21 04:07 tyson-benson

@tyson-benson Came from Google trying to figure out how to camel case my JSON deserialization for the new .NET 5 isolated process functions. Thank you!

kamranayub avatar Jul 30 '21 16:07 kamranayub

So for .Net 6 will this functionality be available for all c# functions or only the isolated process functions?

snapfisher avatar Aug 29 '21 01:08 snapfisher

Is there any news on this ? Now that .NET 6 us supported and the v4 is out

abouroubi avatar Jul 26 '22 17:07 abouroubi

In my recent adventures with this for .NET 6/Function v4 is that it is still case that you can only configure for STJ if running in an isolated process.

Personally, I would like a big hammer that says "AZURE_FUNCTION_PLEASE_CONFIGURE_FOR_STJ_I_AM_AWARE_OF_THE_ISSUES_WITH_JOBJECT_BINDINGS=True".

DarinMacRae avatar Sep 29 '22 18:09 DarinMacRae

'Migrate to System.Text.Json' say all the Microsoft Docs since 2019, yet here we are in March 2023 and Azure Functions doesn't support it.

jeremy-morren avatar Mar 08 '23 07:03 jeremy-morren

Guys, I get it right or I'm missing something ?

HttpClient uses STJ and Azure Function HttpTrigger response uses Newtonsoft ?

CarlosHMoreira avatar Oct 29 '23 21:10 CarlosHMoreira

We ran into the similar case with Function v4, .net 6, & in-process model. (first of all, I'm not a native English speaker... sorry about poor English.)

[FunctionName("DataService")]
public static async Task<IActionResult> DataService(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "dataservice/{action:alpha}")] HttpRequest req, string action)
{
    IActionResult result =  await req.Dispatch(action, DataOptions);
    return result;
}

Our Dispatch method returns various result such as NotFoundResult, BadRequestResult, or JsonResult. The Dispatch must respone dynamically by various clients. For example, for a C# client, Dispatch method returns a JsonResult, such as it include a JsonSerializerOptions which is set to serialize our DTO with rich type information. On the other hand, for a Javascript client, it will returns a JsonResult without JsonSerializerOptions so that Javascript could consume JSON more nativly.

It works very well in ASP.NET Core environment. Howerver, Azure function throws an exception when a JsonResult has a JsonSerializerOptions.

Microsoft.AspNetCore.Mvc.NewtonsoftJson: Property 'JsonResult.SerializerSettings' must be an instance of type 'Newtonsoft.Json.JsonSerializerSettings'.

To workaround, we add an extra helper method.

private static IActionResult ProcessJsonResult(this IActionResult result)
{
    if (result is JsonResult jsonResult)
    {
        object resultValue = jsonResult.Value;
        JsonSerializerOptions options = jsonResult.SerializerSettings as JsonSerializerOptions;
        ContentResult contentResult = new()
        {
            StatusCode = jsonResult.StatusCode,
            ContentType = "application/json",
            Content = JsonSerializer.Serialize(resultValue, options)
        };
        result = contentResult;
    }
    return result;
}

[FunctionName("DataService")]
public static async Task<IActionResult> DataService(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "dataservice/{action:alpha}")] HttpRequest req, string action)
{
    IActionResult result =  await req.Dispatch(action, DataOptions);
    return result.ProcessJsonResult();
}

It works... However, we think that the helper method takes more memory(not a stream!) and is not asynchronous. We had migrated from NJ to STJ since STJ takes less memory and is more faster than NJ!

ksyu33 avatar Jan 05 '24 16:01 ksyu33