Implement validation in Spring Data REST.

Background

Spring Data REST is a framework that helps developers to build hypermedia-driven REST web services. It is built on top of the Spring Data project and makes it easy to build hypermedia-driven REST web services that connect to Spring Data repositories – all using HAL as the driving hypermedia type.

In this article, we will look at how to implement validation in Spring Data REST.

Domain Classes

There are two @Entity classes, Author and Book. Both classes are accompanied by JpaRepository classes - AuthorRepository and BookRepository.

While Author and Book are standard JPA entities, their repositories are annotated with @RepositoryRestResource to expose them as REST resources.

@RepositoryRestResource
interface AuthorRepository extends JpaRepository<Author, UUID> {
}
@RepositoryRestResource
interface BookRepository extends JpaRepository<Book, UUID> {
}

Validation

We will implement a validation that ensures that Author in Book is not INACTIVE. To do this, we will create a custom validator class, BeforeCreateBookValidator.

class BeforeCreateBookValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return Book.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        Book book = (Book) target;

        if (book.getAuthor().getStatus() == INACTIVE) {
            errors.rejectValue("author", "author.inactive", "Author is inactive");
        }

    }

}

As we can see, the validator class implements Validator interface and overrides supports and validate methods. The supports method checks if the class is Book and the validate method checks if the Author is INACTIVE. Next we will inform Spring about our Validator through BookValidatorConfiguration.

@Configuration
class BookValidatorConfiguration implements RepositoryRestConfigurer {

    @Override
    public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
        validatingListener.addValidator("beforeCreate", new BeforeCreateBookValidator());
    }

}

Now, Spring is aware that the Validator will be executed before creating a Book.

Verify Implementation

We will perform a POST request to create a Book with an Author that is INACTIVE. The request will be rejected with 400 Bad Request response.

@Import(TestDataRestValidationApplication.class)
@SpringBootTest(webEnvironment = RANDOM_PORT, properties = "spring.jpa.hibernate.ddl-auto=create-drop")
class CreateBookTests {

    @Autowired
    private TestRestTemplate restClient;

    @Test
    @DisplayName("When I create a Book with an inactive Author, I should get a Bad Request response")
    void inactiveAuthor() {
        var body = """
                {
                  "title": "If",
                  "author": "%s"
                }
                """.formatted(authorUri());

        var response = restClient.exchange("/books", POST, new HttpEntity<>(body, headers()), RepositoryRestErrorResponse.class);

        assertThat(response.getStatusCode()).isEqualTo(BAD_REQUEST);

        assertThat(response.getBody().getErrors())
                .hasSize(1)
                .extracting(ValidationError::getMessage)
                .containsExactly("Author is inactive");
    }

    private URI authorUri() {
        var body = """
                {
                  "name": "Rudyard Kipling",
                  "status": "INACTIVE"
                }
                """;

        return restClient.exchange("/authors", POST, new HttpEntity<>(body, headers()), Void.class)
                .getHeaders()
                .getLocation();
    }

}

Full implementation of the test can be found in CreateBookTests.