Let’s say you are creating a REST API to add a batsman profile to a cricket database.
The API takes in batsman details like name , age ,number of matches, batting average, experience and runs .
Before adding these details to our database you want to validate these details.
Like name should be a valid string.
Age should be a number.
Number of matches should be a number.
Batting average should be a number.
Experience should be a number.
Runs should be a number.
And also let’s say you want to add a custom validation :
“Runs should match number of matches * batting average”
Instead of manually validating these fields on your server app , Spring lets you write minimal code to do these validations.
Here are the steps to implement the same.
First let’s see how to implement validation for a single field.
Then we will see how to implement validation for two fields combined together.
Validation for single fields:
STEP 1: Add spring-boot-starter-validation dependency
STEP 2: Add @Valid annotation before the request body in the REST API
STEP 3: Annotate your Java object representing your request body with @Pattern annotation with specific regular expression
STEP 1: Add spring-boot-starter-validation dependency
In pom.xml add the below dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
STEP 2: Add @Valid annotation before the request body in the REST API
Let’s say you have created the below API to add batsman profile :
@PostMapping("/batsman")
public Batsman addBatsman(@RequestBody Batsman batsman){
// do business logic here like adding to database
return batsman;
}
Now to validate the batsman profile just add @Valid annotation next to @RequestBody annotation like below:
@PostMapping("/batsman")
public Batsman addBatsman(@RequestBody @Valid Batsman batsman){
//do businesss logic here
return batsman;
}
STEP 3: Annotate your Java object representing your request body with @Pattern annotation with specific regular expression
Now let’s look at the batsman java object representing the request:
package com.example.demo;
import jakarta.validation.constraints.Pattern;
public class Batsman {
String name;
String age;
String experience;
String battingAverage;
String matches;
String runs;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getExperience() {
return experience;
}
public void setExperience(String experience) {
this.experience = experience;
}
public String getBattingAverage() {
return battingAverage;
}
public void setBattingAverage(String battingAverage) {
this.battingAverage = battingAverage;
}
public String getMatches() {
return matches;
}
public void setMatches(String matches) {
this.matches = matches;
}
public String getRuns() {
return runs;
}
public void setRuns(String runs) {
this.runs = runs;
}
@Override
public String toString() {
return "Batsman [name=" + name + ", age=" + age + ", experience=" + experience + ", battingAverage="
+ battingAverage + ", matches=" + matches + ", runs=" + runs + "]";
}
}
We want to implement the below validations:
- Name should be a valid string
- Age should be a number
- Experience should be a number
- Batting average should be a number
- Matches should be a number
- Runs should be a number
Adding these validations is as simple as adding @Pattern annotation to these fields and passing the regex .
For example,
To add the validation that the given field should be a number, just add
@Pattern(regexp = “[0-9]*”)
To add the validation that the given field should be a string , just add
@Pattern(regexp = “[a-zA-Z ]*”)
Here is the updated request class:
package com.example.demo;
import jakarta.validation.constraints.Pattern;
public class Batsman {
@Pattern(regexp="[a-zA-Z ]*")
String name;
@Pattern(regexp="[0-9]*")
String age;
@Pattern(regexp="[0-9]*")
String experience;
@Pattern(regexp="[0-9]*")
String battingAverage;
@Pattern(regexp="[0-9]*")
String matches;
@Pattern(regexp="[0-9]*")
String runs;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getExperience() {
return experience;
}
public void setExperience(String experience) {
this.experience = experience;
}
public String getBattingAverage() {
return battingAverage;
}
public void setBattingAverage(String battingAverage) {
this.battingAverage = battingAverage;
}
public String getMatches() {
return matches;
}
public void setMatches(String matches) {
this.matches = matches;
}
public String getRuns() {
return runs;
}
public void setRuns(String runs) {
this.runs = runs;
}
@Override
public String toString() {
return "Batsman [name=" + name + ", age=" + age + ", experience=" + experience + ", battingAverage="
+ battingAverage + ", matches=" + matches + ", runs=" + runs + "]";
}
}
Now if you run the app and give an invalid value for any of the above fields you will get 400 bad request.
For example , I am giving an invalid string for name field as below :
The same holds true for other fields as well.
Now let’s move on to the next part.
Let’s do a custom validation with two fields.
Let’s say we want to validate the runs against the number of matches and batting average.
Runs = batting average * matches
To implement this we need to do a few more steps
In a nutshell , we need to create a custom annotation and add it to the Batsman class.
Here are the additional steps (after adding the starter validation dependency and adding @Valid annotation to the request parameter)
STEP 1: Create an interface for the custom annotation
Here is the interface:
package com.example.demo;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
@Documented
@Constraint(validatedBy = RunsValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidRuns {
String message() default "Runs invalid";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Here @Constraint annotation denotes the class where we are going to add our custom annotation
@Target annotation represents at what level are we going to apply this annotation (class level , method level etc – in our case we are going to apply at the Batsman class level – this is represented by ElementType.TYPE)
STEP 2: Create the custom validation class
package com.example.demo;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class RunsValidator implements ConstraintValidator<ValidRuns, Batsman> {
@Override
public boolean isValid(Batsman batsman, ConstraintValidatorContext context) {
int matches = Integer.parseInt(batsman.getMatches());
int average = Integer.parseInt(batsman.getBattingAverage());
int runs = Integer.parseInt(batsman.getRuns());
if (matches * average != runs) {
return false;
}
return true;
}
}
As you see we are implementing ConstraintValidator interface and implementing isValid method.
We are applying our rule “Runs = matches * average” here.
If it doesn’t match we return false.
Now let’s run our app
In the above request , runs don’t match the matches multiplied by batting average , so error is thrown.
Let’s correct it.
It works fine now.
That’s it!
Leave a Reply