Updated on

To set global JsonSerializerOptions defaults in ASP.NET Core, configure them once at startup: AddJsonOptions() for controllers, ConfigureHttpJsonOptions() for minimal API endpoints, or AddNewtonsoftJson() to hand serialization back to Json.NET.

ASP.NET Core serializes JSON with System.Text.Json by default, not Newtonsoft.Json. That has been true since ASP.NET Core 3.0, and it is what a Web API created on .NET 10 does today. We set up all three below and show which one reaches which endpoints.

To download the source code for this article, you can visit our GitHub repository.

Before we dive into this topic, we recommend going through our article Serialization and Deserialization in C#.

Now, let’s move on.

What Is JsonSerializerOptions?

JsonSerializerOptions is the settings object that System.Text.Json reads every time it serializes or deserializes an object. It lives in the System.Text.Json namespace and travels as a parameter to JsonSerializer.Serialize() and JsonSerializer.Deserialize().

Five properties carry most of the work. PropertyNamingPolicy decides the casing of property names, so JsonNamingPolicy.CamelCase turns ReleaseDate into releaseDate. DefaultIgnoreCondition set to JsonIgnoreCondition.WhenWritingNull drops null properties from the output. Encoder controls how characters are escaped, which matters for HTML-unsafe content. Converters holds the JsonConverter instances that handle types the defaults get wrong, such as a custom date format. WriteIndented adds line breaks and indentation, useful in development and wasteful in production.

In ASP.NET Core we rarely construct this object ourselves. The framework holds an instance per pipeline and hands it to the formatters that write our responses. Setting options globally therefore means reaching for the framework’s options object rather than for a JsonSerializerOptions we new up in a controller.

Three of those five deserve a closer look.

Setting DefaultIgnoreCondition to JsonIgnoreCondition.WhenWritingNull is not only about tidier output: omitting null properties reduces payload size and potentially enhances performance.

The Encoder property helps prevent XSS attacks by properly encoding JSON data, typically using JavaScriptEncoder.Default.

The Converters property is a list of JsonConverter instances that we use to customize the serialization of certain types that do not serialize as expected by default. This is useful for types like DateTime or custom business objects.

The WriteIndented property formats the JSON output with indentations and line breaks, making it more readable. While this property is helpful for readability during development, we usually disable it in production to reduce the payload size.

What Is the Default JSON Serializer in ASP.NET Core?

ASP.NET Core uses System.Text.Json by default. That has been true since ASP.NET Core 3.0, and it is still true in .NET 10, where a fresh dotnet new webapi project serializes with it and installs no JSON package at all.

Newtonsoft.Json was the default before that. ASP.NET Web API 2 on the .NET Framework serialized with Json.NET, and so did ASP.NET Core 1.x and 2.x, which shipped it in the shared framework. That is why so much older sample code and so many older Stack Overflow answers assume it. None of that holds for a project created today.

Newtonsoft.Json is still fully supported, but it is opt-in. Installing the Microsoft.AspNetCore.Mvc.NewtonsoftJson package and calling AddNewtonsoftJson() on AddControllers() swaps the input and output formatters back to Json.NET.

Which serializer is active decides which options object we configure. System.Text.Json reads JsonSerializerOptions; Newtonsoft.Json reads JsonSerializerSettings. Setting properties on the wrong one is the usual reason a global change appears to do nothing at all.

FrameworkDefault JSON serializerHow to change it
ASP.NET Web API 2 (.NET Framework)Newtonsoft.Json (Json.NET)Replace the JsonMediaTypeFormatter
ASP.NET Core 1.x to 2.xNewtonsoft.Json (Json.NET), shipped in the shared frameworkAddJsonOptions() on AddMvc()
ASP.NET Core 3.0 to .NET 10System.Text.JsonAddJsonOptions(), or AddNewtonsoftJson() to switch back

If the library itself is new to us, it is worth starting with an introduction to System.Text.Json through examples before setting anything globally.

How Do We Set Global JSON Options for Controllers?

Controller actions read their JSON settings from Microsoft.AspNetCore.Mvc.JsonOptions. By setting these options once at startup, we guarantee uniform handling of JSON data across every action in our application.

To start with, let’s create a Product class inside our Web API project:

public class Product
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public decimal Price { get; set; }
    public int Quantity { get; set; }
    public DateTime ReleaseDate { get; set; }
    public Manufacturer Manufacturer { get; set; } = new Manufacturer();
}

Also, let’s create a Manufacturer class:

public class Manufacturer
{
    public string? Name { get; set; }
    public string? Location { get; set; }
}

Here, we define two model classes to hold the product’s properties.

Now, let’s configure the JSON serialization options in the Program class:

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
        options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
        options.JsonSerializerOptions.WriteIndented = false;
        options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Default;
        options.JsonSerializerOptions.AllowTrailingCommas = true;
        options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString;
    });

Here, we chain AddJsonOptions() onto AddControllers(), which configures Microsoft.AspNetCore.Mvc.JsonOptions, the options object MVC hands to its formatters.

builder.Services.Configure<JsonOptions>() reaches the same object, but only when the right JsonOptions type is in scope, and which one that is depends on the using directive at the top of the file. The chained form says which options object it configures, so it is the one to prefer.

First, we set all the essential properties. Additionally, we set AllowTrailingCommas to enhance parser flexibility. Next, we enable NumberHandling with JsonNumberHandling.AllowReadingFromString to allow parsing numbers from JSON strings into their appropriate numeric types.

Since one setting here decides the casing of every response the API sends, it is worth reading how serializing property names in camelCase behaves across a whole project, and how writing a custom naming policy extends it to a convention the framework does not ship.

Now, let’s create a ProductController class and define a POST method:

[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
    [HttpPost]
    public ActionResult CreateProduct(Product product)
    {
        return Ok(product);
    }
}

Here, we create a simple POST method that accepts the Product object as the input parameter and return it without any modifications.

Let’s take a look at the product JSON data we send to the API in the request body:

{
  "Id": 1,
  "Name": null,
  "Price": 0,
  "Quantity": "5",
  "ReleaseDate": "2024-04-14T10:49:31.813Z",
  "Manufacturer": {
        "Name":"Apple",
        "Location" : "California"
  }
}

We define the JSON properties using the Pascal case. Also, we set the Name property to null and set the Quantity property in the string format.

Now, let’s inspect the response:

{
    "id": 1,
    "price": 0,
    "quantity": 5,
    "releaseDate": "2024-04-14T10:49:31.813Z",
    "manufacturer": {
        "name": "Apple",
        "location": "California"
    }
}

The JSON serializer changes PascalCase names to camelCase and excludes the Name property due to our JsonIgnoreCondition.WhenWritingNull setting.

The NumberHandling property interprets the Quantity as a number despite its string format, and AllowTrailingCommas lets the parser accept a trailing comma in the payload rather than rejecting it.

How Do We Set Global JSON Options for Minimal APIs?

When we create minimal APIs, it’s essential to have fine-grained control over JSON serialization settings specifically for HTTP responses, which means applying particular JSON serialization settings exclusively to the data sent back to clients in HTTP responses.

The ConfigureHttpJsonOptions() extension method allows us to customize JSON options that apply exclusively to the HTTP pipeline, ensuring these settings are isolated from other application parts.

Moving on, let’s set up the ConfigureHttpJsonOptions() method in the Program class:

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
    options.SerializerOptions.WriteIndented = false;
    options.SerializerOptions.Encoder = JavaScriptEncoder.Default;
    options.SerializerOptions.AllowTrailingCommas = true;
    options.SerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString;
});

Here, we add the ConfigureHttpJsonOptions() method to the IServiceCollection. Similarly, we set the JSON serialization properties as we did previously.

Microsoft’s minimal API documentation states it in one line: “Options can be configured globally for an app by invoking ConfigureHttpJsonOptions.”

Next, let’s define a minimal API in the Program class:

app.MapPost("api/Product/create", (Product product) =>
{
    return product;
});

The serialization settings we defined will apply to this response.

How Do We Use Newtonsoft.Json Globally in ASP.NET Core?

Two steps make Newtonsoft.Json the serializer for a whole Web API. Install the Microsoft.AspNetCore.Mvc.NewtonsoftJson package, then chain AddNewtonsoftJson() onto AddControllers() and set the properties we want on options.SerializerSettings.

That single call replaces the MVC input and output formatters. Every controller action then serializes and deserializes through Json.NET, with no change to the actions themselves and no calls to JsonConvert anywhere in our code.

JsonConvert.DefaultSettings is a different thing, and mixing the two up is a common mistake. It only affects direct calls to JsonConvert.SerializeObject() and JsonConvert.DeserializeObject() that we write ourselves. An action that returns an object still goes through whichever formatter MVC has registered, so setting DefaultSettings alone changes nothing about the response the client receives.

Reach for Newtonsoft.Json when we need what it still does better: DateFormatString, DefaultValueHandling, and a converter ecosystem built up over a decade of use.

Newtonsoft.Json, also called Json.NET, gives us more options for formatting dates and handling null values than System.Text.Json does. For example, it can serialize dates to strings using various formats and handle null values in multiple ways (ignore, include, or convert to a default).

As a first step, let’s install the Microsoft.AspNetCore.Mvc.NewtonsoftJson package:

dotnet add package Microsoft.AspNetCore.Mvc.NewtonsoftJson

With this, let’s register Json.NET as the serializer for every controller in the Program class:

builder.Services.AddControllers()
    .AddNewtonsoftJson(options =>
    {
        options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        options.SerializerSettings.Formatting = Formatting.Indented;
        options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
        options.SerializerSettings.DateFormatString = "dd-MM-yyyy";
        options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Ignore;
    });

We then set the default naming strategy for JSON keys to camel case using CamelCasePropertyNamesContractResolver(). We also format the output JSON with indentations for readability. Next, we omit any null properties in the object from the resulting JSON and serialize the date as strings in the “day-month-year” format.

Finally, we handle the default value for properties by setting it to DefaultValueHandling.Ignore, which causes properties with default values (like 0 for integers, false for booleans, etc.) to be excluded from serialization.

This one call is exclusive: it swaps both MVC formatters, so the System.Text.Json options we set earlier stop applying to controller actions the moment it is registered.

Let’s define a POST endpoint in the ProductController class:

[HttpPost("save")]
public ActionResult SaveProduct(Product product)
{
    return Ok(product);
}

In this action, we return the product object and let the registered formatter serialize it. There is no JsonConvert call anywhere, which is the whole point of configuring the serializer globally.

So, let’s look at the request body:

{
    "Id": 1,
    "Name": null,
    "Price": 0,
    "Quantity":"5",
    "ReleaseDate": "2024-04-14T10:49:31.813Z",
    "Manufacturer":
    {
        "Name":"Apple",
        "Location" : "California"
    }
}

We include JSON properties with Pascal case names and default values for the Name and Price property.

Let’s check the response:

{
  "id": 1,
  "quantity": 5,
  "releaseDate": "14-04-2024",
  "manufacturer": {
    "name": "Apple",
    "location": "California"
  }
}

The Name property is omitted from the response because it contains a null value. Serialization formats the ReleaseDate property in the ‘dd-MM-yyyy’ style, and the Price property is ignored since we have set it to a default value of 0.

Which JSON Options Object Applies to Which Endpoints?

ASP.NET Core carries two separate JSON options objects, and confusingly both types are named JsonOptions.

Microsoft.AspNetCore.Mvc.JsonOptions serves controller actions and exposes its settings through a property called JsonSerializerOptions. Microsoft.AspNetCore.Http.Json.JsonOptions serves minimal API endpoints, Results.Json() and TypedResults, and exposes its settings through a property called SerializerOptions. Which of the two a bare Configure<JsonOptions>() call reaches depends entirely on the using directive at the top of the file.

The two do not cascade. Configuring one leaves the other on its defaults, so a naming policy set through AddJsonOptions() changes what a controller returns and nothing else, and ConfigureHttpJsonOptions() changes what a minimal API endpoint returns and nothing else. An application serving both kinds of endpoint has to configure both.

Our own JsonSerializer.Serialize() calls read neither object. They use the options instance we hand them, or the library defaults when we hand them none, which is why a global change never reaches them.

Which formatter runs at all is decided one step earlier, when content negotiation picks the formatter for the request.

Best Practices and Considerations

When we update JSON serialization settings globally, it’s essential to carefully evaluate the potential effects on our existing codebase to avoid unintended consequences. It can alter the behavior of API endpoints that clients have already consumed, potentially breaking contracts if the clients depend on the existing serialization format. We should gradually implement, backed by comprehensive testing and feature toggles, which can help safeguard against disruptions in system behavior.

Consistency is key in JSON serialization practices across an application. It promotes understandability and helps prevent bugs. We must centralize and document the serialization settings to expect uniform behavior throughout the application. Both JsonOptions types are ordinary options classes, so the options pattern these settings objects use applies to them like any other.

When serialized data forms part of a contract with external systems or requires long-term storage, ensuring compatibility and careful versioning is critical. We should introduce changes through versioned APIs and consider the impact on data storage, processing, and external consumers. This strategic approach will preserve data integrity and support a smooth codebase evolution as new best practices emerge. Property order is part of that contract for some consumers, and controlling the order the properties are written in is another JsonSerializerOptions concern worth settling once.

When considering the future of .NET, we must consider these additional factors. Microsoft’s System.Text.Json is the default serializer for new .NET applications. Even if we choose to use Newtonsoft.Json, we should be aware of potential shifts in best practices and prepare to adapt our strategy as the ecosystem evolves.

Conclusion

In this article, we’ve explored different ways to set up JSON serialization settings within ASP.NET Core, including utilizing the native options provided by System.Text.Json and the more comprehensive features offered by Newtonsoft.Json.

Tested with .NET 10.0.10 (SDK 10.0.302), Newtonsoft.Json 13.0.4 and Microsoft.AspNetCore.Mvc.NewtonsoftJson 10.0.11.