Does anyone know why @Valid fails with javax.validation and works with spring starter validation



I have the following controller:

    @PostMapping( "v1/libraryEvent" )
    public ResponseEntity<LibraryEvent> postLibraryEvent( @RequestBody @Valid LibraryEvent libraryEvent )
          throws JsonProcessingException
    {
        //Does some stuff
    }

which uses this dependency, among others:

import javax.validation.Valid;

the LibraryEvent DTO looks like this:

@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class LibraryEvent
{
    private Integer libraryEventId;

    private LibraryEventType libraryEventType;

    @Valid
    @NotNull
    private Book book;
}

And the book inside that library event, is the following:

@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class Book
{
    @NotNull
    private Integer bookId;
    @NotNull
    private String bookName;
    @NotNull
    private String bookAuthor;
}

I also have this test which uses MockMvc to send a mocked request to the controller in order to test that one:

    @Test
    void postLibraryEvent_4xx() throws Exception {

        //given
        final Book book = Book.builder()
                              .bookId( null )
                              .bookAuthor( null )
                              .bookName( "Kafka using spring boot" )
                              .build();

        final LibraryEvent libraryEvent = LibraryEvent.builder()
                .book( book )
                .build();

        final String json = objectMapper.writeValueAsString( libraryEvent );

        mockMvc.perform(post("/v1/libraryEvent")
                .content(json)
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().is4xxClientError());

    }

Everything is fine so far, the problem is that in my build.gradle I was using javax.validation dependency, and when I ran the test with that dependency, the @Valid annotation did not work because it returned HttpStatus.OK (200) instead of Bad request (400), that is supposed to be, because the id and author of the book were passed as null. This is how my dependencies section of my gradle file was set with javax:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'javax.validation:validation-api:2.0.1.Final'
    testImplementation 'org.awaitility:awaitility:4.0.3'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation ('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

I solved the problem by replacing the javax dependency with the spring boot starter validation one, once I ran the test again after that change, it passed and the @Valid in the controller worked because the test returned 4XX HTTP status. My gradle now looks like this:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    testImplementation 'org.awaitility:awaitility:4.0.3'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation ('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

But this is pretty weird, because spring validation is supposed to use the same validations that javax use or at least the @Valid should behave the same way. So my question is, can anyone explain to me what is the difference between javax validation and spring validation, and why that has failed with javax, and not with spring validation?

Thank you in advance!

Answer

The Spring documentation at https://docs.spring.io/spring-framework/docs/5.3.9/reference/html/core.html#validation-beanvalidation-spring gives the following example for bootstrapping a Bean Validation Provider:

import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class AppConfig {

    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
}

And it goes on with remarking:

A Bean Validation provider, such as the Hibernate Validator, is expected to be present in the classpath and is automatically detected.

The spring-boot-starter-validation dependency seems to do just these two things.



Source: stackoverflow