ASP.NET Core – Add a custom InputFormatter

Input formatters are used to deserialize the request body to a model object (which is then passed into an action method). There are built-in input formatters for handling JSON and XML. You can add your own input formatter when you want to customize request body deserialization.

There are two scenarios where a custom InputFormatter would be useful:

  • You want to receive a Content-Type that’s not supported by the built-in input formatters (such as the text/plain Content-Type).
  • You want to deserialize JSON/XML differently than the settings / custom converters can handle.

To add an input formatter:

  • Subclass InputFormatter (or TextInputFormatter if you need to deal with encodings besides UTF-8)
  • Specify the Content-Type(s).
  • Specify the parameter type(s).
  • Deserialize the request body.

Here’s an example of implementing an input formatter that reads Content-Type “text/csv” for model parameters of type string[]:

using Microsoft.AspNetCore.Mvc.Formatters;

public class CsvStringInputFormatter : InputFormatter
{
	public CsvStringInputFormatter()
	{
		//Which Content-Types this InputFormatter can handle
		SupportedMediaTypes.Add("text/csv");
	}
	public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
	{
		using (var reader = new StreamReader(context.HttpContext.Request.Body))
		{
			string csv = await reader.ReadToEndAsync();
			var values = csv.Split(",");
			return InputFormatterResult.Success(values);
		}
	}
	protected override bool CanReadType(Type type)
	{
		//Which action parameter types this InputFormatter can handle
		return type == typeof(string[]);
	}
}
Code language: C# (cs)

Note: Override CanRead() if you need to check more than just the Content-Type to decide if you can handle the request.

To make the framework use this, add it to the InputFormatters in the initialization code:

using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(o => o.InputFormatters.Add(new CsvStringInputFormatter()));

//rest of init code
Code language: C# (cs)

Now in the controller, add a parameter of the type that the custom input formatter handles (string[] in this case):

[HttpPost()]
[Consumes("text/csv")] //optional: this means only this content-type is allowed
public IActionResult Post([FromBody]string[] names)
{
	return Ok($"Posted {names.Length} name(s)");
}
Code language: C# (cs)

Note: Adding [Consumes] is optional. It restricts the action to only accepting requests with this Content-Type.

To see it work, send a request with Postman and set the Content-Type (to text/csv in this example):

POST /Names

Content-Type: text/csv

Body:
a,b,cCode language: plaintext (plaintext)

This request can be handled by CsvStringInputFormatter because it meets the conditions:

  • Content-Type is text/csv.
  • Parameter type is string[].

It outputs the following 200 (OK) response:

Posted 3 name(s)Code language: plaintext (plaintext)

Return a good error

It returns a 500 error response if an exception is thrown from ReadRequestBodyAsync().

If you return just InputFormatterResult.Failure(), it’ll return a problem details error response (400 – Bad Request). This has a somewhat misleading error message like 400 (Bad Request) – “The names field is required”.

To return a better error message:

  • Add error details to context.ModelState.
  • Return InputFormatterResult.Failure().

Here’s an example:

public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
	using (var reader = new StreamReader(context.HttpContext.Request.Body))
	{
		string csv = await reader.ReadToEndAsync();
		var values = csv.Split(",");

		if (values[0] == "bad")
		{
			context.ModelState.TryAddModelError("Body", "Invalid value in CSV!");
			return InputFormatterResult.Failure();
		}

		return InputFormatterResult.Success(values);
	}
}
Code language: C# (cs)

Now test it by sending a request with Postman that’ll result in an error. In this simple example, sending the word “bad” will cause it to return an error:

POST /Names

Content-Type: text/csv

Body:
bad,a,b,c

This returns the following 400 (Bad Request) response with the error details we added:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "00-3f0ce093d0d2699af60cef301d97c1e4-1c406fad9d15c0b8-00",
    "errors": {
        "Body": [
            "Invalid value in CSV!"
        ]
    }
}
Code language: JSON / JSON with Comments (json)