Overview

Dynamic Validation with Spring Boot Validation

No Comments

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:

@RestController
public class DataController {
    @RequestMapping(value = "/input", method = RequestMethod.POST)
    public ResponseEntity<?>; acceptInput(@Valid @RequestBody Data data ) {
        dataRepository.save(data);
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

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:

public class Data {
    @NotNull
    private final String someStringValue;
    @Min(1)
    private final int someIntValue;
 
    @JsonCreator
    public Data(@JsonProperty("someStringValue") String someStringValue, @JsonProperty("someIntValue") int someIntValue) {
        this.someStringValue = someStringValue;
        this.someIntValue = someIntValue;
    }
 
    public String getSomeStringValue() {
        return someStringValue;
    }
 
    public int getSomeIntValue() {
        return someIntValue;
    }
}

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:

@Component
public class StringValueValidator {
 
    public void validate(String language, Data data, Errors errors) {
        if (!"de-DE".equals(language)) {
            if (data.getSomeStringValue().length() > 140) {
                errors.reject("someStringValue");
            }
        }
    }
}

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:

    @RequestMapping(value = "/validation", method = RequestMethod.POST)
    public ResponseEntity<?> acceptData(@Valid @RequestBody Data data, Errors errors, 
        @RequestHeader(HttpHeaders.ACCEPT_LANGUAGE) String language) {
        stringValueValidator.validate(language, data, errors);
        if (errors.hasErrors()) {
            return new ResponseEntity<>(createErrorString(errors), HttpStatus.BAD_REQUEST);
        }
        return new ResponseEntity<>(HttpStatus.OK);
    }

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.

Ronny Bräunlich

Ronny works since May 2017 for the codecentric AG. He is convinced about TDD and works mostly in the JVM ecosystem.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Comment

Your email address will not be published. Required fields are marked *