1. Overview
This article covers a basic introduction to Spring Data REST Validators. If you need to first go over the basics of Spring Data REST, definitely visit this article to brush up on the basics.
Simply put, with Spring Data REST, we can simply add a new entry into the database through the REST API, but we of course also need to make sure the data is valid before actually persisting it.
This article continues on an existing article and we will reuse the existing project we set up there.
And, if you’re looking to first get started with Spring Data REST – here’s a good way to hit the ground running:
2. Using Validators
Starting with Spring 3, the framework features the Validator interface – which can be used to validate objects.
2.1. Motivation
In the previous article, we defined our entity having two properties – name and email.
And so, to create a new resource, we simply need to run:
curl -i -X POST -H "Content-Type:application/json" -d
'{ "name" : "Test", "email" : "[email protected]" }'
http://localhost:8080/users
This POST request will save the provided JSON object into our database, and the operation will return:
{
"name" : "Test",
"email" : "[email protected]",
"_links" : {
"self" : {
"href" : "http://localhost:8080/users/1"
},
"websiteUser" : {
"href" : "http://localhost:8080/users/1"
}
}
}
A positive outcome was expected since we provided valid data. But, what will happen if we remove the property name, or just set the value to an empty String?
To test the first scenario, we will run modified command from before where we will set empty string as a value for property name:
curl -i -X POST -H "Content-Type:application/json" -d
'{ "name" : "", "email" : "Baggins" }' http://localhost:8080/users
With that command we’ll get the following response:
{
"name" : "",
"email" : "Baggins",
"_links" : {
"self" : {
"href" : "http://localhost:8080/users/1"
},
"websiteUser" : {
"href" : "http://localhost:8080/users/1"
}
}
}
For the second scenario, we will remove property name from request:
curl -i -X POST -H "Content-Type:application/json" -d
'{ "email" : "Baggins" }' http://localhost:8080/users
For that command we will get this response:
{
"name" : null,
"email" : "Baggins",
"_links" : {
"self" : {
"href" : "http://localhost:8080/users/2"
},
"websiteUser" : {
"href" : "http://localhost:8080/users/2"
}
}
}
As we can see, both requests were OK and we can confirm that with 201 status code and API link to our object**.**
This behavior is not acceptable since we want to avoid inserting partial data into a database.
2.2. Spring Data REST Events
During every call on Spring Data REST API, Spring Data REST exporter generates various events which are listed here:
- BeforeCreateEvent
- AfterCreateEvent
- BeforeSaveEvent
- AfterSaveEvent
- BeforeLinkSaveEvent
- AfterLinkSaveEvent
- BeforeDeleteEvent
- AfterDeleteEvent
Since all events are handled in a similar way, we will only show how to handle beforeCreateEvent which is generated before a new object is saved into the database.
2.3. Defining a Validator
To create our own validator, we need to implement the org.springframework.validation.Validator interface with the supports and validate methods.
Supports checks if the validator supports provided requests, while validate method validates provided data in requests.
Let’s define a WebsiteUserValidator class:
public class WebsiteUserValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return WebsiteUser.class.equals(clazz);
}
@Override
public void validate(Object obj, Errors errors) {
WebsiteUser user = (WebsiteUser) obj;
if (checkInputString(user.getName())) {
errors.rejectValue("name", "name.empty");
}
if (checkInputString(user.getEmail())) {
errors.rejectValue("email", "email.empty");
}
}
private boolean checkInputString(String input) {
return (input == null || input.trim().length() == 0);
}
}
Errors object is a special class designed to contain all errors provided in validate method. Later in this article, we’ll show how you can use provided messages contained in Errors object.
To add new error message, we have to call errors.rejectValue(nameOfField, errorMessage).
After we’ve defined the validator, we need to map it to a specific event which is generated after the request is accepted.
For example, in our case, beforeCreateEvent is generated because we want to insert a new object into our database. But since we want to validate object in a request, we need to define our validator first.
This can be done in three ways:
- Add Component annotation with name “beforeCreateWebsiteUserValidator“. Spring Boot will recognize prefix beforeCreate which determines the event we want to catch, and it will also recognize WebsiteUser class from Component name.
@Component("beforeCreateWebsiteUserValidator") public class WebsiteUserValidator implements Validator { ... }
- Create Bean in Application Context with @Bean annotation:
@Bean public WebsiteUserValidator beforeCreateWebsiteUserValidator() { return new WebsiteUserValidator(); }
- Manual registration:
@SpringBootApplication public class SpringDataRestApplication implements RepositoryRestConfigurer { public static void main(String[] args) { SpringApplication.run(SpringDataRestApplication.class, args); } @Override public void configureValidatingRepositoryEventListener( ValidatingRepositoryEventListener v) { v.addValidator("beforeCreate", new WebsiteUserValidator()); } }
- For this case, you don’t need any annotations on WebsiteUserValidator class.
2.4. Event Discovery Bug
At the moment, a bug exists in Spring Data REST – which affects events discovery.
If we call POST request which generates the beforeCreate event, our application will not call validator because the event will not be discovered, due to this bug.
A simple workaround for this problem is to insert all events into Spring Data REST ValidatingRepositoryEventListener class:
@Configuration
public class ValidatorEventRegister implements InitializingBean {
@Autowired
ValidatingRepositoryEventListener validatingRepositoryEventListener;
@Autowired
private Map<String, Validator> validators;
@Override
public void afterPropertiesSet() throws Exception {
List<String> events = Arrays.asList("beforeCreate");
for (Map.Entry<String, Validator> entry : validators.entrySet()) {
events.stream()
.filter(p -> entry.getKey().startsWith(p))
.findFirst()
.ifPresent(
p -> validatingRepositoryEventListener
.addValidator(p, entry.getValue()));
}
}
}
3. Testing
In Section 2.1. we showed that, without a validator, we can add objects without name property into our database which is not desired behavior because we don’t check data integrity.
If we want to add the same object without name property but with provided validator, we will get this error:
curl -i -X POST -H "Content-Type:application/json" -d
'{ "email" : "[email protected]" }' http://localhost:8080/users
{
"timestamp":1472510818701,
"status":406,
"error":"Not Acceptable",
"exception":"org.springframework.data.rest.core.
RepositoryConstraintViolationException",
"message":"Validation failed",
"path":"/users"
}
As we can see, missing data from request was detected and an object was not saved into the database. Our request was returned with 500 HTTP code and message for an internal error.
The error message doesn’t say anything about the problem in our request. If we want to make it more informational, we will have to modify response object.
In the Exception Handling in Spring article, we showed how to handle exceptions generated by the framework, so that’s definitely a good read at this point.
Since our application generates a RepositoryConstraintViolationException exception we will create a handler for this particular exception which will modify response message.
This is ours RestResponseEntityExceptionHandler class:
@ControllerAdvice
public class RestResponseEntityExceptionHandler extends
ResponseEntityExceptionHandler {
@ExceptionHandler({ RepositoryConstraintViolationException.class })
public ResponseEntity<Object> handleAccessDeniedException(
Exception ex, WebRequest request) {
RepositoryConstraintViolationException nevEx =
(RepositoryConstraintViolationException) ex;
String errors = nevEx.getErrors().getAllErrors().stream()
.map(p -> p.toString()).collect(Collectors.joining("\n"));
return new ResponseEntity<Object>(errors, new HttpHeaders(),
HttpStatus.PARTIAL_CONTENT);
}
}
With this custom handler, our return object will have information about all detected errors.
4. Conclusion
In this article, we showed that validators are essential for every Spring Data REST API which provides an extra layer of security for data insertion.
We also illustrated how simple is to create new validator with annotations.
As always, the code for this application can be found in the GitHub project.