Azure-functions-host: Azure Functions do not respect global Json.Net SerializerSettings

Created on 31 Mar 2020  路  4Comments  路  Source: Azure/azure-functions-host

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

Note that this might be better suited for the Azure Webjobs repository, feel free to let me know/move it

I have some JSON I'd like to be able to convert to an object using custom types, e.g. from NodaTime. I want to be able to put the JSON on a queue, and then have it automatically converted to the correct type.

I can do this in a regular app by configuring the global JsonConvert.DefaultSettings method. However when running Azure functions, this is not respected. It seems like Azure Functions uses its own JsonSerializer with its own settings via Microsoft.Azure.WebJobs.Host.Protocols.JsonSerialization,
and thus ignores the users own settings.

Let me demonstrate with a code example that does not work

namespace AIP.BulkDataSimulator
{
    public class FooModel
    {
        public Instant Instant { get; set; }

    }

    public static class Reproduction
    {
        [FunctionName("Send")]
        public static async Task<ActionResult<string>> Send(
            [HttpTrigger(AuthorizationLevel.Function, "get")]
            HttpRequest req,
            [Queue("simulate-queue"), StorageAccount("BulkDataSimulationInternalStorage")]
            IAsyncCollector<string> simulateQueue
        )
        {
            // This would normally enable the Instant to be parsed as json
            JsonConvert.DefaultSettings = () => new JsonSerializerSettings()
                .ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);

            var foo = new FooModel()
            {
                Instant = SystemClock.Instance.GetCurrentInstant()
            };

            await simulateQueue.AddAsync(JsonConvert.SerializeObject(foo));
            return "Added";

        }

        [FunctionName("Receive")]
        public static void Receive(
            [QueueTrigger("simulate-queue", Connection = "BulkDataSimulationInternalStorage")]
            FooModel foo
        )
        {
            // This would normally enable the Instant to be parsed as json
            JsonConvert.DefaultSettings = () => new JsonSerializerSettings()
                .ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);

            Console.WriteLine(JsonConvert.SerializeObject(foo));
        }
    }
}

When the second function is hit, this triggers an error

System.Private.CoreLib: Exception while executing function: Receive. Microsoft.Azure.WebJobs.Host: Exception binding parameter 'foo'. Microsoft.Azure.WebJobs.Extensions.Storage: Binding parameters to complex objects (such as 'FooModel') uses Json.NET serialization. 
1. Bind the parameter type as 'string' instead of 'FooModel' to get the raw values and avoid JSON deserialization, or
2. Change the queue payload to be valid json. The JSON parser failed: Cannot convert value to NodaTime.Instant

Describe the solution you'd like

There's a few issues here. Of course the solution above with setting JsonConvert.DefaultSettings won't work - the settings are set after the object is parsed.

I would however expect that when using dependency injection and setting the settings via a Startup class, that the JsonSerializer settings are respected. This is not the case.

Perhaps you could use only your own settings, if the user has not provided them globally?

Describe alternatives you've considered

Use a custom JsonConverter

Json.Net seems to pick up custom JsonConverters when set as attributes. However the catch is that Json.Net automatically converts the date-looking strings into actual Dates, and Nodatime requires the strings to parse them into Nodatime types. See here for more information.
This means that a custom JsonConverter is an alright solution for most cases, but not this one.

Receive it as a string

There is of course the possibility to receive the parameter as a string. However this means you lose out on a lot of other goodness, such as automatic bindings from the value of the class. For perspective, our actual function signature looks something like this

         public static async Task RunAsync(
             [QueueTrigger("simulate-queue", Connection = "BulkDataSimulationInternalStorage")]
             SimulationItem simulationEvent,
             [Blob("avro-examples/{inputFile}", FileAccess.Read, Connection = "SampleDataStorage")]
             Stream simulationBlob,
             [Blob("ingest-container/{outputFile}", FileAccess.Write, Connection = "IngestStorage")]
             Stream ingestBlob,
             [EventHub("bulk", Connection = "IngestEventHub")]
             IAsyncCollector<byte[]> ingestEventHub,
             CancellationToken token,
             ILogger log
         )

and I'd hate to do all that with dynamic bindings.

breaking

Most helpful comment

Same problem here. I need to set ReferenceLoopHandling.Ignore globally but can't find a way to do that? Anyone found a solution?

As example like:

        public override void Configure(IFunctionsHostBuilder builder)
        {
            JsonConvert.DefaultSettings = () => new JsonSerializerSettings
            {
                Formatting = Formatting.Indented,
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore
            };
        }

All 4 comments

I am also running into this issue, since we want to set additional JsonOptions in our Startup.cs file, but there doesn't seem to be a way to do that. In older functions versions, we could call builder.Services.AddMvcCore().AddJsonFormatter..., and configure options off of that. In Functions v3, the functions won't start if AddMvcCore() is called by our startup.cs.

@mhoeger, is there any way to configure json options in Functions v3 at the moment?

Same problem here. I need to set ReferenceLoopHandling.Ignore globally but can't find a way to do that? Anyone found a solution?

As example like:

        public override void Configure(IFunctionsHostBuilder builder)
        {
            JsonConvert.DefaultSettings = () => new JsonSerializerSettings
            {
                Formatting = Formatting.Indented,
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore
            };
        }

@fabiocav and @brettsam - I'm guessing this isn't something we could enable now (breaking), but wondering if there is a clean workaround you can think of?

Any solutions for this very common issue or at least some info if this is possible in future versions?

Was this page helpful?
0 / 5 - 0 ratings