How to do server side validation in Spring Boot for REST API requests?

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:

  1. Name should be a valid string
  2. Age should be a number
  3. Experience should be a number
  4. Batting average should be a number
  5. Matches should be a number
  6. 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!


Posted

in

,

by

Comments

Leave a Reply

Discover more from The Full Stack Developer

Subscribe now to keep reading and get access to the full archive.

Continue reading