Handling Model Binding Errors when using [FromBody] in .NET Core 2.1

So, I missed this before but I have found here:

https://docs.microsoft.com/en-us/aspnet/core/web-api/index?view=aspnetcore-2.2#automatic-http-400-responses

That if you use the

[ApiController] 

attribute on your controller, it will automatically handle serialisation errors and provide the 400 response, equivalent to:

if (!ModelState.IsValid)
{
    return BadRequest(ModelState);
}

You can turn this behaviour off in the Startup.cs like this:

services.AddMvc()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressModelStateInvalidFilter = true;
    });

If you are looking to customise the response, a better option is to use a InvalidModelStateResponseFactory, which is a delegate taking an ActionContext and returning an IActionResult which will be called to handle serialisation errors.

See this example:

services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = actionContext => 
    {
        var errors = actionContext.ModelState
            .Where(e => e.Value.Errors.Count > 0)
            .Select(e => new Error
            {
            Name = e.Key,
            Message = e.Value.Errors.First().ErrorMessage
            }).ToArray();

        return new BadRequestObjectResult(errors);
    }
});

The framework uses Model Binders to map the request strings into a complex object, so my guess is that you will need to create a Custom Model Binder. Please refer Custom Model Binding in ASP.Net Core

But before that, an easier way to try would be to try Binder attributes in your models. BindRequired attribute adds a model state error if binding cannot occur. So you can modify your model as :

public class Thing 
{
    [BindRequired]
    public string Description {get;set;}

    [BindRequired]
    public int Amount {get;set;}
}

If that doesn't work for you, then you can try to create a custom model binder. An example from the article :

[ModelBinder(BinderType = typeof(AuthorEntityBinder))]
public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string GitHub { get; set; }
    public string Twitter { get; set; }
    public string BlogUrl { get; set; }
}

public class AuthorEntityBinder : IModelBinder
{
   private readonly AppDbContext _db;
   public AuthorEntityBinder(AppDbContext db)
   {
       _db = db;
   }

public Task BindModelAsync(ModelBindingContext bindingContext)
{
    if (bindingContext == null)
    {
        throw new ArgumentNullException(nameof(bindingContext));
    }

    var modelName = bindingContext.ModelName;

    // Try to fetch the value of the argument by name
    var valueProviderResult =
        bindingContext.ValueProvider.GetValue(modelName);

    if (valueProviderResult == ValueProviderResult.None)
    {
        return Task.CompletedTask;
    }

    bindingContext.ModelState.SetModelValue(modelName,
        valueProviderResult);

    var value = valueProviderResult.FirstValue;

    // Check if the argument value is null or empty
    if (string.IsNullOrEmpty(value))
    {
        return Task.CompletedTask;
    }

    int id = 0;
    if (!int.TryParse(value, out id))
    {
        // Non-integer arguments result in model state errors
        bindingContext.ModelState.TryAddModelError(
                                modelName,
                                "Author Id must be an integer.");
        return Task.CompletedTask;
    }

    // Model will be null if not found, including for 
    // out of range id values (0, -3, etc.)
    var model = _db.Authors.Find(id);
    bindingContext.Result = ModelBindingResult.Success(model);
    return Task.CompletedTask;
   }
}

You might also want to look at Model Validation