Serverside validation is not only a way to prevent eventual attacks on a system, it also helps ensure data quality. In the Java environment JSR 303 Bean Validation and the javax.validation
packages provide developers with a standardized way of doing so. Fields that have to fulfill certain criteria receive the corresponding annotations, e.g. @NotNull
, and these are then evaluated by the framework. Naturally, for checking more specific conditions, there is the possibility of creating custom annotations and validators.
The Spring framework has a good Bean Validation integration. It is e.g. possible to validate an incoming request inside a RestController
by adding the @Valid
annotation to the request parameter. This ensures that the incoming object is validated. A simple example is the following controller:
1@RestController
2public class DataController {
3 @RequestMapping(value = "/input", method = RequestMethod.POST)
4 public ResponseEntity<?>; acceptInput(@Valid @RequestBody Data data ) {
5 dataRepository.save(data);
6 return new ResponseEntity<>(HttpStatus.OK);
7 }
8}
When entering the method, the very generic “Data” object has already been completely validated. If a field inside it wasn’t valid, the client would receive a 4xx status code.
Still, there is one disadvantage when using the validations: the annotations are completely static. It is not possible to read information e.g. from the request. Nevertheless, there are ways and means to overcome this limitation and enrich one’s own application with more dynamic validations. To be more specific, we want to extract one or more values from the incoming HttpRequest and vary the validation depending on the values.
More dynamic validation
Not so long ago, a joke went around regarding a famous social media platform’s charater limit. This picture provides a very nice summary.
Our example application shall be based on this use case. When our application receives a request that has the language de-DE
set in its header, the text inside the JSON payload is allowed to be 280 characters long. For every other language we enforce a limit of 140 characters. In order to demonstrate the combination with static validation, the DTO contains a number field, which is being validated, too. More precisely, the object looks like this:
1public class Data {
2 @NotNull
3 private final String someStringValue;
4 @Min(1)
5 private final int someIntValue;
6
7 @JsonCreator
8 public Data(@JsonProperty("someStringValue") String someStringValue, @JsonProperty("someIntValue") int someIntValue) {
9 this.someStringValue = someStringValue;
10 this.someIntValue = someIntValue;
11 }
12
13 public String getSomeStringValue() {
14 return someStringValue;
15 }
16
17 public int getSomeIntValue() {
18 return someIntValue;
19 }
20}
The JSON annotations come from Jackson and are already included in Spring Boot Starter Web, which is quite practical for our example. The someStringValue
, which already has an annotation, shall be the field we use for checking the character limit.
For the validation we need a custom class containing the logic:
1@Component
2public class StringValueValidator {
3
4 public void validate(String language, Data data, Errors errors) {
5 if (!"de-DE".equals(language)) {
6 if (data.getSomeStringValue().length() > 140) {
7 errors.reject("someStringValue");
8 }
9 }
10 }
11}
I would like to emphasize here that the validator class does not implement any javax.validation
interface, not even javax.xml.validation.Validator
. This is because the validation depends on values from the request and is supposed to take place after the rest of the validation. Still, we want to utilize the existing checks (@NotNull
und @Min
). Except for the @Component
annotation, the StringValueValidator
is a POJO.
The Errors
object originates from Spring and has the fully qualified name org.springframework.validation.Errors
. As you can see, in case of a negative test result, we add the field that is being rejected to the Errors
. It is also possible to add a more specific error message there.
Only using the @Valid
annotation in the controller is not sufficient anymore. The existing errors are also needed as an additional parameter. By adding Errors
to the parameter list, Spring recognizes that it should not reject the request immediately and pass the existing validation errors into the method. We have to be careful here because Spring will no longer send an automatic 4xx response in case of validation errors for us. We are now responsible ourselves to return the appropriate status code.
Next to the errors, we let Spring extract the language from the header. Of course, we could access the HttpRequest here but that way we save some effort. The language, the data, and the existing errors are then passed to our StringValueValidator
. The complete request method looks like this:
1@RequestMapping(value = "/validation", method = RequestMethod.POST) 2 public ResponseEntity<?> acceptData(@Valid @RequestBody Data data, Errors errors, 3 @RequestHeader(HttpHeaders.ACCEPT_LANGUAGE) String language) { 4 stringValueValidator.validate(language, data, errors); 5 if (errors.hasErrors()) { 6 return new ResponseEntity<>(createErrorString(errors), HttpStatus.BAD_REQUEST); 7 } 8 return new ResponseEntity<>(HttpStatus.OK); 9 }
We now have a dynamic validation, adapting its behaviour with respect to the request. The language shall only serve as an example placeholder for any value that could be inside the request. Alternatives could be the request URL or values inside the payload.
One of the curious things here is that one would expect to be able to make the validator a RequestScoped bean and then have it injected in the controller. Unfortunately, it was not possible for me to get this approach running. When testing with more than one request, the first one always got “stuck” inside the validator and the test then failed.
You can find the complete example project includung unit tests on GitHub: https://github.com/rbraeunlich/spring-boot-additional-validation
Conclusion
As shown, it is possible to extend the validation of fields with dynamic aspects in a quite simple way. We were even able to combine our extended validation with the existing one without experiencing any constraints. Especially complex validations that cannot be represented by pure annotations can easily be added to a RestController
in this way.
More articles
fromRonny Bräunlich
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog author
Ronny Bräunlich
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.