1

I'm developing a very simple REST API using ASP.NET Core 6.0 - Minimal APIs and for one of Post methods, I need to validate the json body of the request. I used System.ComponentModel.DataAnnotations for that purpose and the code is working fine:

using System.ComponentModel.DataAnnotations;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/API_v1/Send", (PostRequest request) =>
{
    ICollection<ValidationResult> ValidationResults = null;
    if (Validate(request, out ValidationResults))
    {
        //request object is valid and has proper values
        //the rest of the logic...
    }
    return new { status = "failed"};
});

app.Run();

static bool Validate<T>(T obj, out ICollection<ValidationResult> results)
{
    results = new List<ValidationResult>();
    return Validator.TryValidateObject(obj, new ValidationContext(obj), results, true);
}


public class PostRequest
{
    [Required]
    [MinLength(1)]
    public string To { get; set; }
    [Required]
    [RegularExpression("chat|groupchat")]
    public string Type { get; set; }
    [Required]
    public string Message { get; set; }
}

Problem with my code arises when one of the fields in the json request is not of the proper type; For example this sample json body (to is no longer a string):

{
    "to": 12,
    "type": "chat",
    "message": "Hi!"
}

would raise the following error:

Microsoft.AspNetCore.Http.BadHttpRequestException: Failed to read parameter "PostRequest request" from the request body as JSON.
 ---> System.Text.Json.JsonException: The JSON value could not be converted to System.String. Path: $.to | LineNumber: 1 | BytePositionInLine: 12.
 ---> System.InvalidOperationException: Cannot get the value of a token type 'Number' as a string.
   at System.Text.Json.Utf8JsonReader.GetString()
   at System.Text.Json.Serialization.Converters.StringConverter.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   --- End of inner exception stack trace ---
   at System.Text.Json.ThrowHelper.ReThrowWithPath(ReadStack& state, Utf8JsonReader& reader, Exception ex)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCoreAsObject(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonConverter jsonConverter, Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonReaderState& readerState, Boolean isFinalBlock, ReadOnlySpan`1 buffer, JsonSerializerOptions options, ReadStack& state, JsonConverter converterBase)
   at System.Text.Json.JsonSerializer.ContinueDeserialize[TValue](ReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack, JsonConverter converter, JsonSerializerOptions options)
   at System.Text.Json.JsonSerializer.ReadAllAsync[TValue](Stream utf8Json, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(HttpRequest request, Type type, JsonSerializerOptions options, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(HttpRequest request, Type type, JsonSerializerOptions options, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.RequestDelegateFactory.<>c__DisplayClass46_3.<<HandleRequestBodyAndCompileRequestDelegate>b__2>d.MoveNext()
   --- End of inner exception stack trace ---
   at Microsoft.AspNetCore.Http.RequestDelegateFactory.Log.InvalidJsonRequestBody(HttpContext httpContext, String parameterTypeName, String parameterName, Exception exception, Boolean shouldThrow)
   at Microsoft.AspNetCore.Http.RequestDelegateFactory.<>c__DisplayClass46_3.<<HandleRequestBodyAndCompileRequestDelegate>b__2>d.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

HEADERS
=======
Accept: */*
Connection: keep-alive
Host: localhost:5090
User-Agent: PostmanRuntime/7.26.8
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Content-Length: 62
Postman-Token: e31d3575-d2ec-49a7-9bef-04eaecf38a24

Obviously it no longer can cast request into an object of type PostRequest but what is the proper way to handle these kind of situations? (defining request as type object and checking for the presence and type of every property seems ugly)

Further description: I want to now how I can catch the above mentioned error.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
wiki
  • 1,877
  • 2
  • 31
  • 47
  • Aside from the `FromBody`, it's not really clear to me what you're trying to do. Your sample JSON just doesn't match the declaration in `PostRequest`. In this particular case it's just the type of one property, but it's not clear just how broken you would expect the request to be and still be able to handle it. – Jon Skeet Jan 02 '22 at 09:32
  • If the body cannot match `PostRequest` then obviously it will be invalid. But how can I detect the above mentioned scenario? (I mean I can know that it was invalid only when an error was raised that I do not know how to catch!) – wiki Jan 02 '22 at 10:00
  • What do you want to do with an invalid request other than report it as an error? I would suggest that the current behavior is probably the most appropriate already. If you basically want to replace the built-in binding system with your own, you'll probably need to handle requests in a lower-level way. – Jon Skeet Jan 02 '22 at 10:11
  • Yes, I want to report it to the user **properly** (if not possible, completely ignore the request even), but how can I report to the user?! Only thing that user would see now is that big error! – wiki Jan 02 '22 at 10:13
  • I wouldn't expect a user *not running locally, in a development mode* to get that error - I'd expect that just be the ASP.NET Core local development message. Fundamentally, this is an invalid request for that endpoint, so an HTTP 400 response is far more suitable than a 200 with a body of "failed". But basically, if you don't want ASP.NET Core to do the parsing, then you should probably try just accepting an `HttpContext` parameter instead of `PostRequest`. (I haven't tried that, but I'd expect it to work.) – Jon Skeet Jan 02 '22 at 10:27
  • If I let go of `PostRequest` and use `HttpContext` then I will lose all the functionality of `DataAnnotations`!!!. Really frustrating. – wiki Jan 02 '22 at 10:31
  • As I said, I believe you basically need to decide between "I'll let ASP.NET Core do what it thinks is the right thing" or "I'll implement that functionality myself". – Jon Skeet Jan 02 '22 at 15:42

4 Answers4

2

Asp.Net Core provides a way to register exception handlers:

app.UseExceptionHandler(c => c.Run(async context =>
{
    var exception = context.Features
        .Get<IExceptionHandlerFeature>()
        ?.Error;
    if (exception is not null)
    {
        var response = new { error = exception.Message };
        context.Response.StatusCode = 400;

        await context.Response.WriteAsJsonAsync(response);
    }
}));
spzvtbg
  • 964
  • 5
  • 13
2

Minimal API is called that way for a reason - many convenient features related to binding, model state and so on are not present compared to MVC in sake of simplicity and performance.

Maybe there is more convenient approach to your task but for example you can leverage custom binding mechanism to combine the json parsing and validation:

public class ParseJsonAndValidationResult<T>
{
    public T? Result { get; init; }
    public bool Success { get; init; }
    // TODO - add errors

    public static async ValueTask<ParseJsonAndValidationResult<T>?> BindAsync(HttpContext context)
    {
        try
        {
            var result = await context.Request.ReadFromJsonAsync<T>(context.RequestAborted);
            var validationResults = new List<ValidationResult>();
            if (!Validator.TryValidateObject(result, new ValidationContext(result), validationResults, true))
            {
                // TODO - add errors
                return new ParseJsonAndValidationResult<T>
                {
                    Success = false
                };
            }

            return new ParseJsonAndValidationResult<T>
            {
                Result = result,
                Success = true
            };
        }
        catch (Exception ex)
        {
            // TODO - add errors
            return new ParseJsonAndValidationResult<T>
            {
                Success = false
            };
        }

    }
}

And in the MapPost:

app.MapPost("/API_v1/Send", (ParseJsonAndValidationResult<PostRequest> request) =>
{ 
    // analyze the result ...
});
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • Thank you a lot for your great response. I ended up using Json.NET `Schema` library by automatically generating schema base on my annotated `PostRequest` class and validating the json body of `HttpContext` using that schema. – wiki Jan 04 '22 at 11:50
  • 3
    @wiki 1) if you think this is the best answer you should mark it as such (also give it a vote up); 2) why not add your own answer with some code example, I am sure this would be helpful to others (even more important for devs learning web development) – Vagaus Apr 09 '22 at 14:27
0

In program.cs add

services.Configure<JsonOptions>(o => o.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));

And that solved my issue.

0

Adriáns comment worked for me too. I'm running a minimal API in ASP.net on .NET 7 and had problems running a Post API with a json as input in the body and it gave men the exakt same problem Wiki had, and by just adding the converter to the config solved the problem.