Implement custom validators using JSR based validations [Java 8/Spring]

Aman Garg
Nerd For Tech
Published in
7 min readJun 8, 2018

Recently, in our projects, we had a requirement to handle validations on the fields in the Java models sent by the client from the UI as a JSON response.

For the purposes of demonstration, let’s take a sampleDto as our model object. It kind of looks like this

@Getter
@Setter
public class SampleDto {
public Integer sampleInteger;
public String sampleString;
public List<Integer> sampleList;
}

Fairly simple. Notice the use of Lombok For @Getter @Setter annotations which have been used to emphasise brevity. Consider a typical MVC type architecture. This dto/model will be passed in from the user as a JSON response. Using Postman, let us construct the request body.

{
"sampleInteger": 171,
"sampleString": "VALID",
"sampleList": [1,2, 3]
}

For mapping the incoming post request, let’s construct a very simple controller, with an endpoint named test/me. Just the basics.

@RestController
@RequestMapping("test")
public class SampleController {
@PostMapping("me")
public void testMe(@RequestBody SampleDto sampleDto) {
//...our validation logic goes here
}

Now coming in to the business requirement. Say, we want to validate whether Requirement A) every string that has been passed must be in upper case
If condition is not met we throw an exception and stop execution.

How would we go onto achieve this?
There are plenty of ways to do this. Consider the naive approach

public void testMe(@RequestBody SampleDto sampleDto) {
if (null != sampleDto.getSampleString()) {
//handle null
}
if (sampleDto.getSampleString().chars().mapToObj(i -> (char)
i).noneMatch(Character::isLowerCase)) {
//handle all upper case
}
}

Nothing fancy here, simply checking if string is not null and that there’s no occurrence of any lower case character.

Voila! It works.

Now that we’ve handled the requirement, without even realizing that we’ve cluttered our controller logic with business level validations, we should be proud.
Just as we’re about to rejoice, client throws in one more request.

Requirement B) the size of the sampleInteger must be between 7–71

We take some time and quickly realise that it’s an additional effort of about 3–4 lines of code.

We even isolate our validation logic to a different function to enforce separation of concerns.

public void testMe(@RequestBody @Valid SampleDto sampleDto) {
if (isValid(sampleDto)) {
//continue
}
else {
//handle errors
}
}
private boolean isValid(SampleDto sampleDto) {
if (null != sampleDto.getSampleString()) {
return false;
}
if (sampleDto.getSampleString().chars().mapToObj(i -> (char)
i).noneMatch(Character::isLowerCase)) {
return false;
}
if (null != sampleDto.getSampleInteger()) {
return false;
}
if (sampleDto.getSampleInteger() >= 7 &&
sampleDto.getSampleInteger() <= 71) {
return false;
}
return true;
}

Amazing, now we can handle separate validations as and when they come. Moreover our controller logic delegates the validation logic.

The client, seeing you revel in your glory, pulls one out of the blue.

C) The sampleInteger entered by the user has to be a prime

It is now, when we begin to notice the problems with our approach so far.

  • We are dealing with a lot of iff’s and else’s.
  • Where in heavens, do we put our validator function. Are we talking about a separate package? What’s the scope of that function? Who’s to say?
  • Should we have separate validation functions for other dto’s? How many are we talking?
  • If there are five validations for a model field, does that mean we need to put five different blocks of logic in a single function? What if there are five different members?
  • If I want validation for some dto’s and not for others, how does that work?

Clearly, we are in a conundrum. As the size of the project grows, our frustration goes along with it, immensely.

Enter JAVA Bean Validation/JSR

Validating user input is, of course, a super common requirement in most applications, and the Java Bean Validation framework has become the de-facto standard for handling this kind of logic.

JSR 380 is a specification of the Java API for bean validation, part of JavaEE and JavaSE, which ensures that the properties of a bean meet specific criteria,

To setup JSR based annotations is pretty straightforward. Include the dependency on hibernate-validation (with version) and you’re good to go.

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>

How do we use it, you say?

Simply
A: annotate the member field with the annotation of the required validation type
B: let spring know that validation has to be done using @Valid annotation

A: Our sampleDto’s integer field now gets a makeover

@NotNull
@Range(min = 7, max = 71, message = "Size has to be between 7-71")
public Integer sampleInteger;

@Range and @NotNull are inbuilt validators. Let’s use it and not reinvent the wheel. Notice that we have the ability to pass in a custom message too, in case the validation fails.

For a moment, consider that the “size ≤ 71 & size≥7 ” is the only constraint that we’re testing. How does our controller logic get modified?

Since Spring has support for validators, we simply let our method know that validation has to be performed using @Valid annotation

B: @Valid Annotation
According to the Docs, @Valid marks a property, method parameter or method return type for validation cascading. Constraints defined on the object and its properties are be validated when the property, method parameter or method return type is validated.This behavior is applied recursively.

This is important to us. Now we can have validations whenever we want, wherever we want.

@PostMapping(“me”)
public void testMe(@RequestBody @Valid SampleDto sampleDto) {
//...our business logic goes here
}

Notice the @Valid annotation. Now we are up for validation. But Spring by default won’t do anything if the validation fails. Nobody catches the exception. It fails silently. This is definitely not what we want.

To catch it explicitly, we need to pass an interface of the type BindingResult: A
General interface that represents binding results. Extends the interface for error registration capabilities, allowing for a Validator to be applied, and adds binding-specific analysis and model building.

We handle error correction separately. Also the method has been modified to return a string so that we can get it as a response.

Our modified controller

Let’s fire a post request from POSTMAN and see it in action.

We receive our message as a response

What happens when our sampleInteger is well within the constraints? i.e. within 7–71. Let’s check it out.

Smooth.

Our validations work well. Everything is in the right place. There’s no uncluttered controllers, well except for the binding result part which we can handle using a controller advice. No problem there.

Let’s address the elephant in the room, shall we?
What about Custom Validations?

How do I handle string upper case check if it’s not implemented by default.

Welcome to the last part of the article. If you made it this far, kudos. We’re on track to implement custom JSR based annotations while at the same time realising why we had the need to and what were our alternatives.

Let’s handle the “String upper case check”. Let’s follow some steps.

  • Decorate the desired field with the custom annotation. We’ll create it soon.
@NotNull()
@StringUpperCase()
public String sampleString;

We name our custom annotation : StringUpperCase. Let’s create it

  • We create an annotation using @interface notation. For ease, let’s put into a separate package, say, com.medium.login.validation.constraints
@Constraint(validatedBy = "StringUpperCaseValidator.class"
public @interface StringUpperCase{
}
Constraint Valid Annotation

It’s a very simple annotation that has a scope of runtime. Our target is either a field or a parameter, which we appropriately specify. We note the following:

@constraint: the class that contains the validation logic for this constraint
message: will be generated when the constraint is violated
groups: used by validation api. Let’s not touch it.
payload: used by Spring to process proxies. Let’s not touch it either

Very well, once we declare the annotation, we need to create a validator for it. Of course, this makes sense. How does this annotation know what’s the logic for its validation.

  • Final step, let’s create the validation logic, in a separate package. Say
    com.medium.login.validation.validator. Creating a separate package is recommended. It’s what enforces modularity. Changing the validation logic should not involve a change in the controllers.
public class StringUpperCaseValidator implements  
ConstraintValidator<StringUpperCase, String>{

@Overrride
public boolean isValid(String val, ConstraintValidatorContext c){
}
}
Constraint Validator Logic

We implement the constraintValidator interface provided to us by the validation library. Notice that we refer to our previously created annotation “StringUpperCase” and we specify we are validating a string. It can be any object.

There is only one method that is paramount here.
You guessed it, it’s isValid(). You’d realise that the logic for isValid method is similar to what we had during the naive implementation.

That’s it. We’re done. We did it.

Finally, let’s verify if it’s working.

When typed in uppercase, all constraints are satisfied.

While we’re at it, let’s throw in a unit test.

@Test
public void sampleStringNotUpperCaseTest() {
SampleDto sampleDto = new SampleDto();
sampleDto.setSampleList(new ArrayList(Arrays.asList(1,2,3)));
sampleDto.setSampleString("FAIL");
sampleDto.setSampleInteger(23);
// test string as invalid: not in uppercase
Set<ConstraintViolation<SampleDto>> violations =
this.validator.validate(sampleDto);
assertEquals(1, violations.size());
}

This brings us to the end of the article.
All validation logic is in separate package, can be enforced whenever we want, doesn’t interfere with MVC logic.

To hide the bindingResults params from controller, we can set up a controller advice. I’ll write about that soon.

If you liked the article, kindly clap and share for the benefit of others alike.
Have a great day.

--

--