Skip to content

Make required keyword handling produce validation error in minimal APIs #64139

@KristofferStrube

Description

@KristofferStrube

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

When we have a model like this:

public class Order
{
    [Required]
    public required string OrderNumber { get; set; }
}

and use it in a Controller API like so:

[ApiController]
[Route("[controller]")]
public class OrderController : ControllerBase
{
    [HttpPost(Name = "Make Order")]
    public IActionResult Post(Order order)
    {
        Console.WriteLine(order.OrderNumber.ToUpper());

        return Ok("Order made");
    }
}

Then when we call it:

POST {{ControllerAPI_HostAddress}}/Order/
Content-Type: application/json
Accept: application/json
{}

We get back some Problem Details:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "$": [
      "JSON deserialization for type 'ControllerAPI.Order' was missing required properties including: 'orderNumber'."
    ],
    "order": [
      "The order field is required."
    ]
  },
  "traceId": "00-b3e09b166ef942bc0d6e50fc2368e4ef-b69710dd2bd4f14d-00"
}

But when we try to make a similar setup with Minimal API:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddValidation(); // Using the new validation

var app = builder.Build();

app.MapPost("/order", (Order order) =>
{
    Console.WriteLine(order.OrderNumber.ToUpper());

    return Results.Ok("Order made");
})
.WithName("Make Order");

app.Run();

And call it with the same payload:

POST {{MinimalAPI_HostAddress}}/Order/
Content-Type: application/json
Accept: application/json
{}

We instead get back:

Microsoft.AspNetCore.Http.BadHttpRequestException: Failed to read parameter "Order order" from the request body as JSON.
 ---> System.Text.Json.JsonException: JSON deserialization for type 'MinimalAPI.Order' was missing required properties including: 'orderNumber'.
   at System.Text.Json.ThrowHelper.ThrowJsonException_JsonRequiredPropertyMissing(JsonTypeInfo parent, BitArray assignedOrNotRequiredPropertiesSet)
   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, Boolean& isPopulatedValue)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, T& value, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.ContinueDeserialize[TReadBufferState,TStream](TReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack, T& value)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsync[TReadBufferState,TStream](TStream utf8Json, TReadBufferState bufferState, CancellationToken cancellationToken)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsObjectAsync(PipeReader utf8Json, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(HttpRequest request, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(HttpRequest request, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.RequestDelegateFactory.<HandleRequestBodyAndCompileRequestDelegateForJson>g__TryReadBodyAsync|102_0(HttpContext httpContext, Type bodyType, String parameterTypeName, String parameterName, Boolean allowEmptyRequestBody, Boolean throwOnBadRequest, JsonTypeInfo jsonTypeInfo)
   --- 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.<HandleRequestBodyAndCompileRequestDelegateForJson>g__TryReadBodyAsync|102_0(HttpContext httpContext, Type bodyType, String parameterTypeName, String parameterName, Boolean allowEmptyRequestBody, Boolean throwOnBadRequest, JsonTypeInfo jsonTypeInfo)
   at Microsoft.AspNetCore.Http.RequestDelegateFactory.<>c__DisplayClass102_2.<<HandleRequestBodyAndCompileRequestDelegateForJson>b__2>d.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

Expected Behavior

I would also expect to get back Problem Details for Minimal API.

And if we were in a really ideal world, then it would simply return that the OrderNumber field was required for both Controller API and Minimal API:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "OrderNumber": [
      "The OrderNumber field is required."
    ]
  },
  "traceId": "00-516cb8a80977b468f9cb7a0ba121048f-001d81939c66676a-00"
}

But I understand that it would be technically challenging because the object needs to be deserialized before it can be validated against its DataAnnotations. But I imagine that many will combine the usage of the [Required] attribute and the required keyword because the alternative is not beautiful (it feels dirty to write default!):

public class Order
{
    [Required]
    public string OrderNumber { get; set; } = default!;
}

Steps To Reproduce

I have made a minimal repo for this here: https://github.com/KristofferStrube/ModelValidationInApiWithRequiredProperty

In the repo, I have also added tests that show that the Minimal API does not return problem details in the specific scenario.

Exceptions (if any)

No response

.NET Version

10.0.100-rc.2.25502.107

Anything else?

No response

Metadata

Metadata

Assignees

Labels

area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etc

Type

No type
No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions