How to Control JSON Result with ASP.NET Core Custom Code

When you are creating HTTP APIs, you want to have control over how you are responding to requests in terms of altering the status codes and the format of the body. In this blog post, we will be looking at how we can control the JSON response by customizing the status code in ASP.NET Core.

How ASP.NET Core Decides the Response Format

Let’s first have a look at how a JSON result is actually produced by ASP.NET Core, which will help us understand the mechanism behind response formatting. When we scaffold an ASP.NET Core project with Web API configuration with the below dotnet CLI command:

dotnet new webapi --no-https --auth=None

Our startup.cs file will look like below:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

We also have a sample controller file named as WeatherForecastController.cs, which has the below shape:

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

This has the basics we can work with to understand how the response is shaped up in terms of format and status code.

When we configure ASP.NET Core with services.AddControllers, this adds the built-in OutputFormatters, which are used to write an object to the output stream:

  • Microsoft.AspNetCore.Mvc.Formatters.HttpNoContentOutputFormatter
  • Microsoft.AspNetCore.Mvc.Formatters.StringOutputFormatter
  • Microsoft.AspNetCore.Mvc.Formatters.StreamOutputFormatter
  • Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter

These output formatters are what allows an action to return any object return value. The formatter is selected here through the process called content negotiation, which occurs when the client specifies an Accept header.

Let’s look at this with an example. With the below modification, we will add the XML output formatter to the list of supported formatters:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddXmlSerializerFormatters();
}

Now, we will see that the response will be written in XML when we specify that we want to receive the response body in XML through the Accept header:

curl -v -H "Accept: application/xml" http://localhost:5000/WeatherForecast
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> GET /WeatherForecast HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.54.0
> Accept: application/xml
> 
< HTTP/1.1 200 OK
< Date: Mon, 13 Jan 2020 21:06:32 GMT
< Content-Type: application/xml; charset=utf-8
< Server: Kestrel
< Content-Length: 829
< 
* Connection #0 to host localhost left intact
<ArrayOfWeatherForecast xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><WeatherForecast><Date>2020-01-14T21:06:32.786281+00:00</Date><TemperatureC>3</TemperatureC><Summary>Hot</Summary></WeatherForecast><WeatherForecast><Date>2020-01-15T21:06:32.794902+00:00</Date><TemperatureC>36</TemperatureC><Summary>Mild</Summary></WeatherForecast><WeatherForecast><Date>2020-01-16T21:06:32.794907+00:00</Date><TemperatureC>-14</TemperatureC><Summary>Scorching</Summary></WeatherForecast><WeatherForecast><Date>2020-01-17T21:06:32.794908+00:00</Date><TemperatureC>5</TemperatureC><Summary>Chilly</Summary></WeatherForecast><WeatherForecast><Date>2020-01-18T21:06:32.794908+00:00</Date><TemperatureC>-13</TemperatureC><Summary>Scorching</Summary></WeatherForecast></ArrayOfWeatherForecast>

The response will be in JSON format when we specify the Accept header as application/json:

curl -v -H "Accept: application/json" http://localhost:5000/WeatherForecast
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> GET /WeatherForecast HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.54.0
> Accept: application/json
> 
< HTTP/1.1 200 OK
< Date: Mon, 13 Jan 2020 21:09:05 GMT
< Content-Type: application/json; charset=utf-8
< Server: Kestrel
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
[{"date":"2020-01-14T21:09:05.635107+00:00","temperatureC":4,"temperatureF":39,"summary":"Hot"},{"date":"2020-01-15T21:09:05.635279+00:00","temperatureC":-8,"temperatureF":18,"summary":"Scorching"},{"date":"2020-01-16T21:09:05.635281+00:00","temperatureC":42,"temperatureF":107,"summary":"Balmy"},{"date":"2020-01-17T21:09:05.635281+00:00","temperatureC":27,"temperatureF":80,"summary":"Chilly"},{"date":"2020-01-18T21:09:05.635282+00:00","temperatureC":44,"temperatureF":111,"summary":"Warm"}]%

Apart from returning POCOs (Plain Old CLR Objects) and letting the content negotiation decide which output formatter to choose, you can also return an IActionResult, which defines a contract that represents the result of an action method, from the controller action which can allow you to have direct control over the return type. For example, built-in helper IActionResult implementation JsonResult returns JSON-formatted data, regardless of the Accept header.

[HttpGet]
public IActionResult Get()
{
    var rng = new Random();
    var result = Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();

    return new JsonResult(result);
}
curl -v -H "Accept: application/xml" http://localhost:5000/WeatherForecast
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> GET /WeatherForecast HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.54.0
> Accept: application/xml
> 
< HTTP/1.1 200 OK
< Date: Mon, 13 Jan 2020 21:18:39 GMT
< Content-Type: application/json; charset=utf-8
< Server: Kestrel
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
[{"date":"2020-01-14T21:18:40.678848+00:00","temperatureC":12,"temperatureF":53,"summary":"Scorching"},{"date":"2020-01-15T21:18:40.685278+00:00","temperatureC":23,"temperatureF":73,"summary":"Chilly"},{"date":"2020-01-16T21:18:40.685283+00:00","temperatureC":24,"temperatureF":75,"summary":"Cool"},{"date":"2020-01-17T21:18:40.685284+00:00","temperatureC":-10,"temperatureF":15,"summary":"Hot"},{"date":"2020-01-18T21:18:40.685284+00:00","temperatureC":-5,"temperatureF":24,"summary":"Bracing"}]%

Altering the Response Status Code

Now, let’s look at how we can change the response status code. With all the cases we have mentioned above, we can simply set Response.StatusCode right before returning the result to the appropriate status code we want to respond with:

[HttpGet]
public IActionResult Get()
{
    var rng = new Random();
    var result = Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();

    Response.StatusCode = StatusCodes.Status400BadRequest;
    return new JsonResult(result);
}

You can see in the below request example that we get the response back with 400 status code:

curl -v -H "Accept: application/json" http://localhost:5000/WeatherForecast
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> GET /WeatherForecast HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.54.0
> Accept: application/json
> 
< HTTP/1.1 400 Bad Request
< Date: Mon, 13 Jan 2020 21:34:53 GMT
< Content-Type: application/json; charset=utf-8
< Server: Kestrel
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
[{"date":"2020-01-14T21:34:54.415857+00:00","temperatureC":21,"temperatureF":69,"summary":"Sweltering"},{"date":"2020-01-15T21:34:54.4239+00:00","temperatureC":-6,"temperatureF":22,"summary":"Freezing"},{"date":"2020-01-16T21:34:54.423907+00:00","temperatureC":9,"temperatureF":48,"summary":"Mild"},{"date":"2020-01-17T21:34:54.423907+00:00","temperatureC":51,"temperatureF":123,"summary":"Freezing"},{"date":"2020-01-18T21:34:54.423908+00:00","temperatureC":49,"temperatureF":120,"summary":"Hot"}]% 

Apart from this simple way of setting the status code, we also have some helper methods on the ControllerBase object, which gives us the ability to shape a response. For example, the Conflict method creates a ConflictObjectResult that produces a Status409Conflict response.

[HttpGet]
public IActionResult Get()
{
    var rng = new Random();
    var result = Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();

    return Conflict(result);
}
curl -v -H "Accept: application/json" http://localhost:5000/WeatherForecast
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> GET /WeatherForecast HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.54.0
> Accept: application/json
> 
< HTTP/1.1 409 Conflict
< Date: Mon, 13 Jan 2020 21:38:03 GMT
< Content-Type: application/json; charset=utf-8
< Server: Kestrel
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
[{"date":"2020-01-14T21:38:04.665259+00:00","temperatureC":-19,"temperatureF":-2,"summary":"Chilly"},{"date":"2020-01-15T21:38:04.673328+00:00","temperatureC":47,"temperatureF":116,"summary":"Bracing"},{"date":"2020-01-16T21:38:04.673334+00:00","temperatureC":28,"temperatureF":82,"summary":"Chilly"},{"date":"2020-01-17T21:38:04.673335+00:00","temperatureC":17,"temperatureF":62,"summary":"Warm"},{"date":"2020-01-18T21:38:04.673335+00:00","temperatureC":49,"temperatureF":120,"summary":"Bracing"}]%

In this case, no matter what we set Response.StatusCode to within the action, it would be overridden by the Conflict helper method.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *