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!
Advertisement
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.