Azure / Azure/azure-functions-host

Azure Functions do not respect global Json.Net SerializerSettings

Open
#5,841 20 comments 20 reactions 1 assignee Assigned to @brettsam View on GitHub
breaking-change
Dominant language
C#
Stars
2k
Forks
482
Avg merge
2d 12h
Merged PRs (30d)
38

Description

#### 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
```csharp
namespace AIP.BulkDataSimulator
{
public class FooModel
{
public Instant Instant { get; set; }

}

public static class Reproduction
{
[FunctionName("Send")]
public static async Task> Send(
[HttpTrigger(AuthorizationLevel.Function, "get")]
HttpRequest req,
[Queue("simulate-queue"), StorageAccount("BulkDataSimulationInternalStorage")]
IAsyncCollector 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](https://nodatime.org/1.2.x/userguide/serialization) 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
```csharp
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 ingestEventHub,
CancellationToken token,
ILogger log
)
```
and I'd hate to do all that with dynamic bindings.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.