ASP.NET Core – How to change the JSON serialization settings

System.Text.Json is the default JSON serializer in ASP.NET Core. It uses the following default serialization settings:

var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, NumberHandling = JsonNumberHandling.AllowReadingFromString };
Code language: C# (cs)

To change the settings at the service level for all controllers, call AddJsonOptions() in Startup.ConfigureServices() like this:

public class Startup { //rest of class public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddJsonOptions(j => { j.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); //rest of method } }
Code language: C# (cs)

Note: This example is passing in JsonStringEnumConverter, which makes JsonSerializer use the enum name instead of enum value.

When you change the settings at the service level, it applies to all controllers. When a request comes in with JSON, it’ll apply your settings when deserializing the request. Likewise, when you are returning a response, it’ll use your settings to serialize the model in the response.

You can also change settings at the action level (but only for serialization) and at the controller level, which I’ll show below.

Change JSON settings at the action level (serialization only)

Let’s say you want to change the JSON serialization settings for a specific action in a controller so that it uses JsonStringEnumConverter.

There are two ways to do that: return JsonResult and pass it a JsonSerializerOptions object, or by directly using JsonSerializer. I’ll show both approaches below.

There are a few flaws with this approach.

  • You can’t change the settings for deserialization.
  • The JsonResult constructor’s options parameter is of type object. It actually requires a JsonSerializerOptions object, but doesn’t enforce that at compile time, which means it’s type unsafe and can lead to runtime exceptions.
  • You have to create new instances of JsonSerializerOptions each time, which is bad because reusing the same options object leads to a 200x speedup in serialization. Note: Alternatively you could add a static property in the controller, or perhaps dependency inject a singleton options object.

You might want to consider this approach only as a last resort. If you only need to change a single action, this might be fine. If you need to change multiple actions in a controller, I’d recommend using the controller level approach instead.

Option 1 – Return JsonResult

You can customize serialization by returning a JsonResult and passing in a JsonSerializerOptions object, like this:

using System.Text.Json; using System.Text.Json.Serialization; using System.Net; [HttpGet("{symbol}")] public async Task<IActionResult> Get(string symbol) { var stock = await GetStockFromRepo(symbol); var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); options.Converters.Add(new JsonStringEnumConverter()); return new JsonResult(stock, options) { StatusCode = (int)HttpStatusCode.OK }; }
Code language: C# (cs)

Note: Notice it’s passing in JsonSerializerDefaults.Web into the constructor. This is to make sure it’s using the defaults that ASP.NET Core normally uses.

When this endpoint is called, the framework uses the options object you passed in to serialize the response object. Notice it has the enum name instead of the value:

{ "symbol": "AMZN", "price": 101.1, "quoteTime": "2021-07-23T15:13:16.3911373-04:00", "fundType": "Stock" }
Code language: JSON / JSON with Comments (json)

Option 2 – Directly use JsonSerializer

Nothing is stopping you from directly using JsonSerializer with your own JsonSerializerOptions object:

using System.Text.Json; using System.Text.Json.Serialization; using System.Net; [HttpGet("{symbol}")] public async Task<IActionResult> Get(string symbol) { var stock = await GetStockFromRepo(symbol); var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); options.Converters.Add(new JsonStringEnumConverter()); return new ContentResult() { StatusCode = (int)HttpStatusCode.OK, ContentType = "application/json", Content = JsonSerializer.Serialize<Stock>(stock, options) }; }
Code language: C# (cs)

When the endpoint is called, it returns the following JSON with the enum name instead of the value:

{ "symbol": "AMZN", "price": 101.1, "quoteTime": "2021-07-23T15:39:11.4887762-04:00", "fundType": "Stock" }
Code language: JSON / JSON with Comments (json)

This is the most tedious approach, but it does give you full control.

Change JSON settings at the controller level (including deserialization)

Let’s say you want to change the JSON settings for all actions in the following controller named StocksController:

[ApiController] [Route("[controller]")] public class StocksController : ControllerBase { [HttpGet("{symbol}")] public async Task<IActionResult> Get(string symbol) { var stock = await GetStockFromRepo(symbol); return Ok(stock); } [HttpPost()] public async Task<IActionResult> Post(Stock stock) { await SaveToRepo(stock); return Ok($"Posted stock {stock.Symbol}"); } }
Code language: C# (cs)

You want the JSON settings to apply to serialization (GET response) and deserialization (POST request).

Notice that StocksController only deals with the Stock model. Let’s assume no other controllers deal with the Stock model. This means you can create a custom converter for the Stock type and pass it into the JSON settings at the service level. When the framework needs to handle serialization / deserialization of the Stock type, it’ll delegate to your custom converter. This effectively means the custom converter will be used specifically for handling serialization / deserialization for StocksController.

With this approach, you don’t have to modify the controller. This allows you to adhere to the Open-Closed Principle, which states that code should be open to extension, but not modification.

I’ll show step by step how to do this approach below.

Step 1 – Create the custom converter

Create the custom converter for the Stock type, add a JsonSerializerOptions property called ConverterOptions, and implement Read() and Write() as wrappers for using JsonSerializer directly with the ConverterOptions.

using System.Text.Json; using System.Text.Json.Serialization; public class StocksConverter : JsonConverter<Stock> { private readonly JsonSerializerOptions ConverterOptions; public StocksConverter(JsonSerializerOptions converterOptions) { ConverterOptions = converterOptions; } public override Stock Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { //Very important: Pass in ConverterOptions here, not the 'options' method parameter. return JsonSerializer.Deserialize<Stock>(ref reader, ConverterOptions); } public override void Write(Utf8JsonWriter writer, Stock value, JsonSerializerOptions options) { //Very important: Pass in ConverterOptions here, not the 'options' method parameter. JsonSerializer.Serialize<Stock>(writer, value, ConverterOptions); } }
Code language: C# (cs)

A few things:

  • Reusing a JsonSerializerOptions object leads to 200x faster serialization and deserialization. The purpose of the ConverterOptions property is to be able to reuse it repeatedly.
  • You can pass in ConverterOptions as a constructor parameter, or just hardcode it. I prefer passing it in.
  • As noted in the comments in Read() / Write(), do not pass in options to Deserialize() / Serialize(). The reason is because options contains a reference to your custom converter. If you tried to use this, it’d result in an infinite loop.

Step 2 – Pass in the custom converter at the service level

In AddJsonOptions(), pass a new JsonSerializerOptions object to an instance of StocksConverter. Use whatever settings you want. This example is using JsonStringEnumConverter.

Then add the StocksConverter object to the main JsonSerializerOptions.

using System.Text.Json; using System.Text.Json.Serialization; public class Startup { //rest of class public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddJsonOptions(j => { var stockConverterOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); stockConverterOptions.Converters.Add(new JsonStringEnumConverter()); var stockConverter = new StocksConverter(stockConverterOptions); j.JsonSerializerOptions.Converters.Add(stockConverter); }); //rest of method } }
Code language: C# (cs)

Step 3 – Send requests to see it work

Send a GET request (note: I’m using Postman):

GET https://localhost:12345/Stocks/AMZN
Code language: plaintext (plaintext)

This returns the following JSON. Notice it’s using the enum name instead of the value, which means it correctly used StocksConverter with the custom settings:

{ "symbol": "AMZN", "price": 101.1, "quoteTime": "2021-07-23T16:57:15.7972445-04:00", "fundType": "Stock" }
Code language: JSON / JSON with Comments (json)

Send a POST request using the enum name:

POST https://localhost:12345/Stocks/AMZN Body: { "symbol": "AMZN", "price": 102.34, "quoteTime": "2021-07-23T16:57:15.7972445-04:00", "fundType": "Stock" }
Code language: plaintext (plaintext)

This returns the following response:

Status: OK Body: Posted stock AMZN
Code language: plaintext (plaintext)

How do we know it used StocksConverter with the custom settings? Because System.Text.Json doesn’t handle enum names by default. If it were using the default serializer settings, it would’ve resulted in this error response:

{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "0HMAE3K7U3GFM:00000001", "errors": { "$.fundType": [ "The JSON value could not be converted to Models.FundTypes. Path: $.fundType | LineNumber: 4 | BytePositionInLine: 23." ] } }
Code language: JSON / JSON with Comments (json)

Leave a Comment