The Error
HTTP 400 Bad Request
The request field is required. The email field is not a valid e-mail address.
Quick Fix - 2 Minutes
// Dev Fix: Add validation to Minimal API.NET 8
app.MapPost("/users", ([FromBody] CreateUserDto dto) =>
{
var validationResults = new List();
var context = new ValidationContext(dto);
if (!Validator.TryValidateObject(dto, context, validationResults, true))
{
return Results.ValidationProblem(validationResults
.ToDictionary(k => k.MemberNames.First(), v => new[] { v.ErrorMessage }));
}
return Results.Ok();
})
.AddEndpointFilter(async (context, next) =>
{
// Dev Fix: Auto-validate using DataAnnotations
var dto = context.GetArgument(0);
var errors = new List();
Validator.TryValidateObject(dto, new ValidationContext(dto), errors, true);
if (errors.Any()) return Results.ValidationProblem(errors.ToDictionary(
e => e.MemberNames.First(), e => new[] { e.ErrorMessage }));
return await next(context);
});
public record CreateUserDto(
[Required] string Name,
[EmailAddress] string Email
);
Why This Happens
Minimal APIs don't auto-validate like Controllers. You must call Validator.TryValidateObject or add endpoint filters..NET 8 won't return 400 unless you handle it.
Real-World Scenario: Controller vs Minimal API 400 Differences
Same DTO, different behavior. This confuses devs migrating to.NET 8 Minimal APIs:
// Controllers: Auto 400 with [ApiController]
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpPost]
public IActionResult Create(UserDto dto) // Validation runs automatically
{
// If dto.Email is invalid, returns 400 ProblemDetails before hitting this line
return Ok();
}
}
// Minimal API: No auto validation. Returns 200 with invalid data unless you add filter
app.MapPost("/users", (UserDto dto) =>
{
// dto.Email = "not-an-email" will reach here. No 400 thrown.
return Results.Ok(dto);
});
// Dev Fix: Add global validation filter for all Minimal APIs
app.AddEndpointFilterFactory((endpointFilterFactoryContext, next) =>
{
return async invocationContext =>
{
var param = invocationContext.Arguments.FirstOrDefault(a => a?.GetType().IsClass == true);
if (param!= null)
{
var results = new List();
var ctx = new ValidationContext(param);
if (!Validator.TryValidateObject(param, ctx, results, true))
{
return Results.ValidationProblem(results.ToDictionary(
r => r.MemberNames.FirstOrDefault()?? "",
r => new[] { r.ErrorMessage?? "" }));
}
}
return await next(invocationContext);
};
});
Key difference: [ApiController] adds model state validation + auto 400. Minimal APIs skip it for performance. You opt into validation with filters.
Related Fixes You Should Know
400 Bad Request usually means these issues too:
- Model Binding Null.NET 8 - You get 200 OK but DTO is null. Missing
[FromBody]or wrongContent-Type. Add validation filter to catch it early. - JSON Deserialization Error - Body has
{ "name": "John" }but C# wantsName. Case mismatch = 400. UsePropertyNameCaseInsensitive = true. - System.Text.Json Required Property -.NET 8
requiredkeyword. Missing field throws 400 withJsonExceptioninstead of validation error. - Minimal API JSON Options Not Applied - You set camelCase in
AddControllers()but Minimal API uses different options. 400 because property names don't match.
FAQ
Q: Why doesn't Minimal API return 400 for invalid DTOs like Controllers do?
Performance. Controllers run model binding + validation + filters for every request. Minimal APIs skip validation by default. You opt in with AddEndpointFilter or manual Validator.TryValidateObject.
Q: How do I get ProblemDetails format like Controllers in Minimal API?
Return Results.ValidationProblem(). It generates RFC 7807 ProblemDetails JSON. Don't use Results.BadRequest() with strings - clients expect structured errors.
Best Practice for.NET 8
- Use
AddEndpointFilterfor reusable validation - Return
Results.ValidationProblem()for ProblemDetails format - Install
FluentValidationfor complex rules - Test with Swagger - invalid body shows 400 automatically
No comments yet. Be the first to share your thoughts!