ASP.NET Core – Add your own InputFormatter to customize request body deserialization

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 this is 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 (and existing customization mechanisms – like a JSON custom converter – are insufficient).

To add an input formatter, subclass InputFormatter (or TextInputFormatter if you need to deal with encodings besides UTF-8), specify which Content-Type(s) it handles, which parameter types it can deserialize to, and then deserialize the request body stream into a model object.

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 context (besides the Content-Type) to determine if your input formatter 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 the right Content-Type (text/csv in this case):

POST /Names Content-Type: text/csv Body: a,b,c
Code language: plaintext (plaintext)

Because the Content-Type is text/csv and the action parameter type is string[], it meets the conditions to use the CsvStringInputFormatter we added. It outputs the following 200 (OK) response:

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

Return a good error

If an unhandled exception is thrown from ReadRequestBodyAsync(), it returns a 500 (Server Error) response with the exception information.

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

If you want to return a better error message, you can add error details to context.ModelState and return InputFormatterResult.Failure(), like this:

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 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)

Leave a Comment