Dynamic JSON Serialization in System.Text.Json

My son would say JSON and Serialization go together like peas and ketchup. I however only find that combination acceptable when it’s meatloaf night. The rest of us who deal with JSON know that it’s a great tool for passing some things around, but you don’t keep it as a string once you are dealing with it in your applications. To that end, we have created DynamicJsonPropertyNamingPolicy.

Enough meatloaf, what’s the problem?

In our current environment at ACV, we have a wide array of technologies in use. Unfortunately, several of them like to serialize and deserialize JSON in different ways. Our Python services like snake_case, our C# services like PascalCase, our JavaScript front ends and others like camelCase.

There are some ways to handle this today with things like JsonPropertyName that allow you to handle this at a low level. However, this can be tiresome if you need to create 3-4 implementations for different ways to serialize the same data or you have different clients who want the data in different formats. You can take a Backend-for-frontend (BFF) approach but that might be overkill just for serialization formats.

Following inspiration from examples specifically like this for JSON.net there was already a few ways to handle this. However, we didn’t find an existing one for the new System.Text.Json libraries introduced in .NET Core 3.0.

How we fixed the problem

Enter: DynamicJsonPropertyNamingPolicy (source here)

How to use it

For output

Now it would be nice if you could just use:

public void ConfigureServices(IServiceCollection services)
{
    // THIS IS WRONG!
    services.AddControllers()
            .AddJsonOptions(options => 
               options.JsonSerializerOptions.PropertyNamingPolicy 
               = new DynamicJsonPropertyNamingPolicy());
}

However, since that happens once at startup, it’s not possible to have it be dynamic and the JsonSerializerOptions handles metadata and caching for the different types you serialize. This provides additional performance improvements, but limits our options for a situation like we have today.

So instead, it has to be handled at each controller method directly:

return new JsonResult(result, HttpContext.GetJsonSerializerOptions());

or from some kind of base class if you want to make things consistent across several controllers:

public abstract class SerializingBaseController : ControllerBase
{
    protected JsonResult JsonResult(object result)
    {
        return new JsonResult(result, HttpContext.GetJsonSerializerOptions());
    }
}

Then just note the lack of new for the returns then:

public class WeatherController : SerializingControllerBase
{
...
    [HttpGet]
    public ActionResult<IEnumerable<WeatherForecast>> Get()
    {
        ...
        return JsonResult(result, HttpContext.GetJsonSerializerOptions());
    }
}

For input

Luckily, handling input dynamically is much easier. This is thanks to the TextInputFormatter class. Creating a derived type in the DynamicJsonPropertyNamingPolicy package allows for easy usage in the application setup:

builder.Services.AddControllers(o =>
{
    o.InputFormatters.Insert(0, new DynamicSystemTextJsonInputFormatter());
});

Well that was fun

In addition to being a exercise in catching null values when binding to data models when crossing systems, this also became our first published nuget package as well as one of the first open source projects here at ACV. Many more to come, so keep you eyes open on our repos (and our jobs!)