Configuring JSON Responses With ASP.NET
[C#, .NET, ASP.NET]
Writing an API with ASP.NET is a very trivial exercise.
Assuming we have this type:
public record Spy(string FullName, string ActiveAgency, byte OfficialAge, bool Active);
We can write an endpoint that returns a Spy
like this:
app.MapGet("/", () =>
{
var spy = new Spy("James Bond", "MI-6", 40, true);
return spy;
}
);
This is short-hand for the following:
app.MapGet("/", () =>
{
var spy = new Spy("James Bond", "MI-6", 40, true);
return Results.Ok(spy);
}
);
Here, we explicitly indicate that we are returning the response with an HTTP 200 response.
Both the above will return the following:
{
"fullName": "James Bond",
"activeAgency": "MI-6",
"officialAge": 40,
"active": true
}
Suppose, for whatever reason, we want to customize this JSON response.
Let us say that the target consumer expects the OfficialAge
to be string
delimited. And that the target consumer, for some reason, expects the attributes to be lowercase snake_cased, as people in the Python community are used to, as opposed to the default camelCase.
This is trivial to achieve.
Rather than just returning the object or using Results.OK, we use the Results.Json method and pass a configured JsonSerializerOptions object to it.
app.MapGet("/Formatted", () =>
{
var spy = new Spy("James Bond", "MI-6", 40, true);
// Setup our options here
var options = new JsonSerializerOptions
{
// Output numbers as strings
NumberHandling = JsonNumberHandling.WriteAsString,
// Use lower case, kebab case
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
return Results.Json(spy, options);
}
);
This will return the following:
{
"full_name": "James Bond",
"active_agency": "MI-6",
"official_age": "40",
"active": true
}
The difference is as follows:
If you have many endpoints, doing this for every endpoint can get tedious. Plus, it will be a nightmare to maintain if you have to change, as you need to remember to check all the endpoints and update your configuration.
You can configure your application to do this by default by configuring the WebApplicationBuilder:
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.NumberHandling = JsonNumberHandling.WriteAsString;
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
});
Once this is done, all endpoints will use these settings.
You can, however, override them on a case-by-case basis.
Our final program looks like this:
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http.Json;
var builder = WebApplication.CreateBuilder(args);
// Configure global json handling
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.NumberHandling = JsonNumberHandling.WriteAsString;
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
});
var app = builder.Build();
app.MapGet("/", () =>
{
var spy = new Spy("James Bond", "MI-6", 40, true);
return Results.Ok(spy);
}
);
app.MapGet("/Formatted", () =>
{
var spy = new Spy("James Bond", "MI-6", 40, true);
// Override the global handling to use UPPER-CASE-KEBAB
var options = new JsonSerializerOptions
{
// Output numbers as strings
NumberHandling = JsonNumberHandling.Strict,
// Use lower case, kebab case
PropertyNamingPolicy = JsonNamingPolicy.KebabCaseUpper,
};
return Results.Json(spy, options);
}
);
app.Run();
If we run the default path, /
, we get the following response:
{
"full_name": "James Bond",
"active_agency": "MI-6",
"official_age": "40",
"active": true
}
If we run the /Formatted
, we get the following:
{
"FULL-NAME": "James Bond",
"ACTIVE-AGENCY": "MI-6",
"OFFICIAL-AGE": 40,
"ACTIVE": true
}
TLDR
You can configure ASP.NET to control the output of JSON responses, either at the global level or at the endpoint level, using an appropriately configured JsonSerializerOptions
object.
The code is in my GitHub.
Happy hacking!