ASP.NET Core JSON API Cheat Sheet

8 October 2019

Some lessons learned building a JSON API using ASP.NET Core.

Recently I've been working on a web API written using ASP.NET Core, with data transferred in JSON format.

ASP.NET Core has built-in functionality for converting between JSON and .NET objects, based on the popular Json.NET library (although in ASP.NET Core 3.0, released a couple of weeks ago, this has moved to the newer System.Text.Json library).

While building this web API I have discovered a number of things concerning JSON APIs and ASP.NET Core. This article is a mixed bag of tips and tricks which may be useful for you (or for me in a few months' time).

Parameter binding

An ASP.NET Core controller method might have a number of arguments. The values of these arguments can be fetched from a number of places, and you can control which using the following attributes:

The ASP.NET Core docs on this are pretty comprehensive as to how this works.

You can omit these attributes and it will try to guess where the values can be found, but I think it's better to be explicit.

The ApiController attribute

My first attempt at using [FromBody] to deserialize JSON didn't go very well. I found that if the request body didn't have the correct structure, or wasn't even valid JSON, then the controller method argument was simply set to null.

This was a surprise to me! I was expecting either an unhandled exception (resulting in a 500 Internal Server Error), or a 400 Bad Request containing details of what was wrong.

I discovered what I was missing: ApiControllerAttribute.

If you decorate your controller with [ApiController], then (among other things) you get JSON validation up-front. That is, if the request body isn't valid JSON, or cannot be deserialized into an object of the correct type, then the server responds with a 400 Bad Request and the response body contains details of the problem.

For more information about this attribute, check out this article on StrathWeb.

The basic Visual Studio template contains this, but it's easy to forget when adding a new controller yourself.

Custom error format

This is great, but the JSON response body isn't particularly friendly, and most likely won't match the format your API uses for errors.

Fortunately you can customise this, and it's pretty easy to do. What you're looking for is ApiBehaviorOptions.InvalidModelStateResponseFactory, which can be set at application startup. Add the following to your ConfigureServices method in the Startup class:

services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = actionContext =>
    {
        var error = // TODO: create your error object here

        return new BadRequestObjectResult(error);
    }
});

Required properties

In many cases, the JSON schema requires a certain property to be present. You can alert the deserializer to this by using the [JsonRequired] attribute on the property. If you do this and the incoming JSON does not contain the property, the deserialization fails.

Another way of doing this is to use [JsonProperty(Required = Required.Always)]. This allows you to specify one of four behaviours relating to required properties (see here for more details) which gives you even greater control over the deserialization. The [JsonProperty] attribute allows you to customize loads of other behaviours too; check out the docs for more info.

The Required property also controls how null values are serialized. If you want to set that globally, you can add the following in your ConfigureServices method:

services.AddMvc()
    .AddJsonOptions(o =>
    {
        o.SerializerSettings.NullValueHandling = // TODO: pick a value
    });

Disallow integers for enumeration values

Often your JSON schema will contain enumerations: properties which have a small collection of valid string values. These have a natural mapping to C# enums, and Json.NET handles that for you out of the box.

However, I found that if I passed in an integer to an enumeration property, deserialization would succeed and the integer would be cast to the enum type. This is generally not a good idea, because then an API consumer could pass in any integer and you'd end up with an invalid value.

You can disable this behaviour during startup by adding the following to your ConfigureServices method:

services.AddMvc()
    .AddJsonOptions(o =>
    {
        o.SerializerSettings.Converters.Add(new StringEnumConverter { AllowIntegerValues = false });
    });

This will convert the JSON value to the appropriate enum value using a case-insensitive comparison on the name of the enum values, but will reject any integer value.

This is a global setting, but you can apply it to a single property using the [JsonProperty] attribute (again, see the docs for more information).

Custom converters

This leads us nicely into a discussion of converters. Out of the box, Json.NET handles conversion between JSON and C# for many different data types, including strings, numbers and booleans.

But what if you want to use an unsupported C# data type? A good example is NodaTime's LocalDate, which would be represented in JSON in ISO 8601 format.

It's easy to create custom converters; there's a good article in the Json.NET docs which shows you how. Then you just need to register it at startup as follows:

services.AddMvc()
    .AddJsonOptions(o =>
    {
        o.SerializerSettings.Converters.Add(new MyCustomConverter());
    });

Swashbuckle

Swashbuckle is a tool which creates API documentation directly from your ASP.NET Core project. It can generate OpenAPI documentation which you can release to the consumers of your API, which lets them know exactly how to use the API. It also comes with a UI for the documentation, giving even more ways for consumers to discover your API.

The Getting Started instructions are pretty good so I suggest you start there.

NSwag

NSwag combines the features of Swashbuckle with the ability to generate C# code for consuming an API. All you need is the OpenAPI spec for the API you would like to consume, and you can easily generate client code in a number of ways. I found the "connected service" approach to be good; I'd recommend How to generate C# or TypeScript client code for OpenAPI (Swagger) specification if you'd like to try it. There are a number of other tutorials linked on the project's GitHub page.

This is not required if you're just exposing a JSON API, but if you're doing that you may well be calling other APIs (for example, as part of a microservices architecture), so I felt it was worth a mention. You could also use this to generate a C# client library to release to the consumers of your API.

Further reading