Skip to content

Improve the experience of handling validation errors in controller methods #26219

Closed
@Sam-Kruglov

Description

@Sam-Kruglov

Affects: 5.3.1
Relates to #23107

Basically, I have the exact same feeling as the author did but he never clarified his point.

You can validate a DTO using @Valid annotation (whether it is built from a bunch of query parameters or from the body) but you can't validate literals like that. So, to validate a request parameter or a path parameter, one has to apply @Validated annotation on the controller class level to enable the AOP proxy for method validation, then it will pick up any JSR 380 annotations.

So, here is a little example. Given the following definition:

class UserDto { @Email String email; }

@RestController
@RequestMapping("users")
@Validated
class UserController {
  
  @PostMapping
  void createUser(@Valid @RequestBody UserDto userDto) {...}
  
  @GetMapping("{email}")
  UserDto getUser(@Email @PathVariable String email) {...}

If we send the following requests, we will get these results:

--------->
POST users
{
  "email": "invalid-email"
}

<---------
throws MethodArgumentNotValidException

--------->
GET users/invalid-email
<---------
throws ConstraintViolationException

Now, handling these 2 is not very nice although we want the same result in the end:

class ValidationErrorResponse { List<Violation> violations; }
class Violation { String fieldName, message; }

  @ExceptionHandler
  ResponseEntity<ValidationErrorResponse> onConstraintValidationException(ConstraintViolationException e) {
    ValidationErrorResponse error = new ValidationErrorResponse();
    for (ConstraintViolation violation : e.getConstraintViolations()) {
      error.getViolations().add(
        new Violation(violation.getPropertyPath().toString(), violation.getMessage()));
    }
    return ResponseEntity.badRequest().body(error);
  }

  @ExceptionHandler
  ResponseEntity<ValidationErrorResponse> onMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    ValidationErrorResponse error = new ValidationErrorResponse();
    for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
      error.getViolations().add(
        new Violation(fieldError.getField(), fieldError.getDefaultMessage()));
    }
    return ResponseEntity.badRequest().body(error);
  }

So, I see 3 inconveniences:

  • I have to always specify @Valid annotation whenever I have a @RequestBody. Can't we just assume I want to validate all bodies? I feel like I might forget it in one place and it won't be validated. Or maybe we could add @Valid onto the DTO class itself and look there for it? Or maybe we could just scan all fields and see if any field has the @constraint annotation on it?
  • I have to always specify @validated annotation on the controller class level to inspect the literals. Why doesn't it inspect them by default since it already searches for @Valid/@validated on the body?
  • Error handling is cumbersome

Metadata

Metadata

Assignees

Labels

in: webIssues in web modules (web, webmvc, webflux, websocket)type: enhancementA general enhancement

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions