1

I'm trying to use the Hibernate Validation in a Spring REST controller with the @Valid annotation.

But the validation is not happening.

Here is what the Maven build has to say:

java.lang.AssertionError: Status expected:<400> but was:<201>

I should get a bad request http status when trying to post a form with invalid fields values, but instead the form posts just fine and the resource is created.

My validation tests tries to post with a missing email value and a too short login name value:

@Test
public void testValidation() throws Exception {
    HttpHeaders httpHeaders = Common.createAuthenticationHeaders("stephane" + ":" + PASSWORD);

MvcResult resultPost = this.mockMvc.perform(
        post("/admin").headers(httpHeaders)
        .contentType(MediaType.APPLICATION_JSON)
        .accept(MediaType.APPLICATION_JSON)
        .content("{ \"firstname\" : \"" + admin0.getFirstname() + "\", \"lastname\" : \"" + admin0.getLastname() + "\", \"email\" : \"" + admin0.getEmail() + "\", \"login\" : \"" + admin0.getLogin() + "\", \"password\" : \"" + admin0.getPassword() + "\", \"passwordSalt\" : \"" + admin0.getPasswordSalt() + "\" }")
    ).andDo(print())
    .andExpect(status().isBadRequest())
    .andExpect(jsonPath("$.firstname").value(admin0.getFirstname()))
    .andExpect(jsonPath("$.lastname").value(admin0.getLastname()))
    .andExpect(jsonPath("$.email").value(""))
    .andExpect(jsonPath("$.login").value("short"))
    .andExpect(jsonPath("$.password").value(admin0.getPassword()))
    .andExpect(jsonPath("$.passwordSalt").value(admin0.getPasswordSalt()))
    .andExpect(header().string("Location", Matchers.containsString("/admin/")))
    .andReturn();

}

I also have an exception handling class:

@ControllerAdvice
public class AdminExceptionHandler {

    @ExceptionHandler(AdminNotFoundException.class)
    @ResponseBody
    public ResponseEntity<ErrorInfo> adminNotFoundException(HttpServletRequest request, AdminNotFoundException e) {
        String url = request.getRequestURL().toString();
        String errorMessage = localizeErrorMessage("error.admin.not.found", new Object[] { e.getAdminId() });
        return new ResponseEntity<ErrorInfo>(new ErrorInfo(url, errorMessage), HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public ResponseEntity<ErrorFormInfo> methodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e) {
        String url = request.getRequestURL().toString();
        String errorMessage = localizeErrorMessage("error.admin.invalid.form.argument");
        ErrorFormInfo errorFormInfo = new ErrorFormInfo(url, errorMessage);
        BindingResult result = e.getBindingResult();
        List<FieldError> fieldErrors = result.getFieldErrors();
        errorFormInfo.getFieldErrors().addAll(populateFieldErrors(fieldErrors));
        return new ResponseEntity<ErrorFormInfo>(errorFormInfo, HttpStatus.BAD_REQUEST);
    }

    public List<ErrorFormField> populateFieldErrors(List<FieldError> fieldErrorList) {
        List<ErrorFormField> errorFormFields = new ArrayList<ErrorFormField>();
        StringBuilder errorMessage = new StringBuilder("");
        for (FieldError fieldError : fieldErrorList) {
            errorMessage.append(fieldError.getCode()).append(".");
            errorMessage.append(fieldError.getObjectName()).append(".");
            errorMessage.append(fieldError.getField());
            errorFormFields.add(new ErrorFormField(fieldError.getField(), localizeErrorMessage(errorMessage.toString())));
            errorMessage.delete(0, errorMessage.capacity());
        }
        return errorFormFields;
    }

    private String localizeErrorMessage(String errorCode) {
        return localizeErrorMessage(errorCode, null);
    }

    private String localizeErrorMessage(String errorCode, Object args[]) {
        Locale locale = LocaleContextHolder.getLocale();
        String errorMessage = messageSource.getMessage(errorCode, args, locale);
        return errorMessage;
    }

}

Note that the AdminNotFoundException is triggered fine by some other test.

But the MethodArgumentNotValidException is not triggered.

Here is the controller class:

@Controller
@ExposesResourceFor(Admin.class)
@RequestMapping("/admin")
public class AdminController {

    @RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public ResponseEntity<Admin> add(@RequestBody @Valid Admin admin, UriComponentsBuilder builder) {
        AdminCreatedEvent adminCreatedEvent = adminService.add(new CreateAdminEvent(admin.toEventAdmin()));
        HttpHeaders responseHeaders = new HttpHeaders();
        responseHeaders.setLocation(builder.path("/admin/{id}").buildAndExpand(adminCreatedEvent.getAdminId()).toUri());
        Admin createdAdmin = adminResourceAssembler.toResource(adminCreatedEvent.getEventAdmin());
        ResponseEntity<Admin> responseEntity = new ResponseEntity<Admin>(createdAdmin, responseHeaders, HttpStatus.CREATED);
        return responseEntity;
    }

}

The versions of the dependencies are 3.2.5.RELEASE for Spring and 5.1.1.Final for hibernate-validator and 3.0.0 for javax.el-api

The same issue occurs when running manually against a Tomcat 7 server and issuing a curl post request:

mvn clean install tomcat7:run -DskipTests

curl -H "Accept:application/json" --user joethebouncer:mignet http://localhost:8080/learnintouch-rest/admin -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d "{ \"firstname\" : \"Stephane\", \"lastname\" : \"Eybert\", \"email\" : \"\", \"login\" : \"short\", \"password\" : \"mignet\", \"passwordSalt\" : \"7bc7bf5f94fef7c7106afe5c3a40a2\" }"

Any clue ?

Here is the whole source code in case someone feels like trying to run this Maven build: http://www.learnintouch.com/learnintouch-data.tar.gz http://www.learnintouch.com/learnintouch-rest.tar.gz

Kind Regards,

Stephane Eybert

Stephane
  • 11,836
  • 25
  • 112
  • 175
  • Why do you have `initBinder` in both the controller and the controller advice? What happens when you leave it in one of the two? – geoand May 11 '14 at 09:39
  • Good point, I just removed the one from the controller. I had added it after searching around, but yes, it is one too many. And after removing it, the issue remains the exact same. – Stephane May 11 '14 at 16:20
  • Also, in my exception handler class, I inject an adminValidator but never use it. And the binding is done not on the injected one. It is messy in there... After I remove this adminValidator from this class the issue remain the exact same though. – Stephane May 11 '14 at 16:36
  • Unfortunately I don't have an idea about why it's not working as expected... – geoand May 11 '14 at 17:16
  • I saw some doing an explicit override instead of my attempt at using a @ExceptionHandler(MethodArgumentNotValidException.class) at http://stackoverflow.com/questions/16651160/spring-rest-errorhandling-controlleradvice-valid which makes me wonder if what I attempt to do is legit. – Stephane May 11 '14 at 17:21
  • I'll simplify my original question by removing the custom validator, since it has no side effect on the issue. – Stephane May 11 '14 at 17:23
  • @MarkusMalkusch I don't think I need one. I'm using a JSR303 one. As stated in the documentation page you linked to: "To configure such a JSR-303 backed Validator with Spring MVC, simply add a Bean Validation provider, such as Hibernate Validator, to your classpath. Spring MVC will detect it and automatically enable Bean Validation support across all Controllers.". My Maven dependencies should be enough. – Stephane May 11 '14 at 17:48
  • I added a link to the project source code. – Stephane May 17 '14 at 14:22
  • I'll give it a shot and let you know :) – geoand May 17 '14 at 14:27

2 Answers2

2

Have you tried using Hibernate Validator 4.2.0.Final? Hibernate Validator 5 is the reference implementation of bean validation API 1.1 (JSR 349) which isn't the same specification than JSR 303.

I assume that Spring Framework 3.2 supports only JSR 303 because I couldn't find any information about the bean validation API 1.1 from its reference manual.

pkainulainen
  • 1,458
  • 1
  • 19
  • 51
  • @Petri Thanks for the comment ! Indeed I had tried that before. So I reverted back to it after what you say. But using hibernate-validator 4.2.0.Final and validation-api 1.1.0.Final and javax.el-api 3.0.0 produces the exact same issue. – Stephane May 14 '14 at 08:11
1

I tried your code, followed the same steps you mention and the validation did occur correctly.

This is the response I got back using the exact same curl command as you posted is:

{"url":"http://localhost:8080/learnintouch-rest/admin","message":"The admin form arguments were invalid.","fieldErrors":[{"fieldName":"login","fieldEr ror":"Length.admin.login"},{"fieldName":"email","fieldError":"NotEmpty.admin.email"}]}

I would suggest that you try clearing you maven repository and build the project from scratch.

I should also mention that in your testValidation() integration test the admin0 object that you are using is actually valid so it makes sense that it passes the validation :)

geoand
  • 60,071
  • 24
  • 172
  • 190
  • Thanks for that. Indeed, my test was faulty. I put the dummy values of empty email and short login name in the expect part of the response, instead of in the admin sent in the request. Of course, it did validate. Blind me ! I didn't even have to clear the repository. The build worked fine after fixing the test like: .content("{ \"firstname\" : \"" + admin0.getFirstname() + "\", \"lastname\" : \"" + admin0.getLastname() + "\", \"email\" : \"\", \"login\" : \"short\", \"password\" : \"" + admin0.getPassword() + "\", \"passwordSalt\" : \"" + admin0.getPasswordSalt() + "\" }") – Stephane May 18 '14 at 07:41
  • Something bit me here, when I beautified the code and renamed the Admin resource into an AdminResource resource. Sure enough I forgot to rename the Length.admin.login message property in the properties file so as to have it as Length.adminResource.login. I wonder if there is a way to customize that name in the code and not have it with the camel cased resource name. – Stephane Jun 28 '14 at 13:32