Custom Validation

Custom validation lets you write your own rule when the standard annotations (@NotBlank, @Min, @Email) are not enough.It is used for application-specific rules, such as “a student is eligible only if the age is 17 to 25 and the marks reach the course cutoff”.You create an annotation, link it to a validator class that holds the logic, and put the annotation on your model.Spring runs it with @Valid like any standard rule, and a failure produces the same 400 response with a field-level message.

Features

  • Your own annotation (@AdmissionEligible) that works exactly like the built-in ones.
  • Rules can use several fields together (age, marks and course), which standard annotations can’t do.
  • Messages are attached to a specific field, so the client sees "marks": "...".
  • Reusable: the annotation can go on any class.
  • Runs after the standard rules, and skips fields they already reported as missing.
  • No extra classes: the validator is nested inside the annotation file.

Custom Validation Types

Type How it works Used for
Field-level custom annotation The annotation sits on one field, and the validator checks that single value. A single-value rule, such as a mobile number or an ID format
Class-level custom annotation The annotation sits on the class, and the validator sees the whole object. A rule across several fields, such as age + marks + course
Programmatic validation in the service You write an if check and throw an exception yourself. A rule that needs the database, such as “email already exists”

Hands-on Experiment: Creating a spring boot application with Custom Validation

The following steps are involved in developing this hands-on experiment.

Step 1: Generate the Project

Step 2: Project Structure

Step 3: pom.xml

Step 4: application.properties

Step 5: Create Entity Class

Step 6: Create Repository Interface

Step 7: Create Service Class

Step 8: Create Controller Class

Step 9: Main Application Class

Step 10: Run the Main Application Class

Step 1: Configure the Project on Spring Initializr

Go to start.spring.io and set:

  • Project: Maven
  • Language: Java
  • Spring Boot version: 3.2.x
  • Group: com.example
  • Artifact: customvalidation
  • Name: customvalidation
  • Package name: com.example.customvalidation
  • Packaging: Jar
  • Java: 17
  • Dependencies: Spring Web, Spring Data JPA, MySQL Driver, Validation I/O.

Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.

Step 2: Generated Project Structure

Step 3: Explore pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>student-custom-validation-demo</artifactId>
    <version>1.0.0</version>
    <name>student-custom-validation-demo</name>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Step 4: application.properties

spring.application.name=student-custom-validation-demo
server.port=8082

spring.datasource.url=jdbc:mysql://localhost:3306/student_admission_db?createDatabaseIfNotExist=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialectCode language: JavaScript (javascript)

Step 5: Create Entity Class

package com.example.customvalidation.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

@Entity
@Table(name = "students")
@AdmissionEligible   // our custom rule, checked after the standard ones
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank(message = "Name is required")
    private String name;

    @NotNull(message = "Age is required")
    private Integer age;

    @NotNull(message = "Marks are required")
    @Min(value = 0, message = "Marks must be between 0 and 100")
    @Max(value = 100, message = "Marks must be between 0 and 100")
    private Integer marks;

    @NotBlank(message = "Course is required")
    private String course;

    public Student() {
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }

    public Integer getMarks() { return marks; }
    public void setMarks(Integer marks) { this.marks = marks; }

    public String getCourse() { return course; }
    public void setCourse(String course) { this.course = course; }
}

Create below Annotation

package com.example.customvalidation.entity;

import jakarta.validation.Constraint;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import jakarta.validation.Payload;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// Custom validation annotation. Put it on the Student class to check admission eligibility.
// The validator logic is nested inside, so the whole custom rule lives in this one file.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = AdmissionEligible.EligibilityValidator.class)
public @interface AdmissionEligible {

    // These three members are required by Bean Validation for every custom annotation
    String message() default "Student is not eligible for admission";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    // The actual rule
    class EligibilityValidator implements ConstraintValidator<AdmissionEligible, Student> {

        @Override
        public boolean isValid(Student student, ConstraintValidatorContext context) {
            // Missing values are reported by @NotNull / @NotBlank, so skip them here
            if (student.getAge() == null || student.getMarks() == null || student.getCourse() == null) {
                return true;
            }

            context.disableDefaultConstraintViolation();
            boolean valid = true;

            // Rule 1: age must be 17 to 25
            if (student.getAge() < 17 || student.getAge() > 25) {
                addError(context, "age", "Age must be between 17 and 25 for admission");
                valid = false;
            }

            // Rule 2: marks must reach the cutoff of the chosen course
            Integer cutoff = switch (student.getCourse()) {
                case "Engineering" -> 70;
                case "Science" -> 65;
                case "Commerce" -> 55;
                case "Arts" -> 50;
                default -> null;
            };

            if (cutoff == null) {
                addError(context, "course", "Course must be one of: Engineering, Science, Commerce, Arts");
                valid = false;
            } else if (student.getMarks() < cutoff) {
                addError(context, "marks", "Marks " + student.getMarks()
                        + " are below the cutoff of " + cutoff + " for " + student.getCourse());
                valid = false;
            }
            return valid;
        }

        // Attaches the message to a specific field, so the client sees field-level errors
        private void addError(ConstraintValidatorContext context, String field, String message) {
            context.buildConstraintViolationWithTemplate(message)
                    .addPropertyNode(field)
                    .addConstraintViolation();
        }
    }
}

Step 6: Create below Repository Interface

package com.example.customvalidation.repository;

import com.example.customvalidation.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
}

Step 7: Create below Service class

package com.example.customvalidation.service;

import com.example.customvalidation.entity.Student;
import com.example.customvalidation.repository.StudentRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class StudentService {

    private final StudentRepository studentRepository;

    public StudentService(StudentRepository studentRepository) {
        this.studentRepository = studentRepository;
    }

    public Student admit(Student student) {
        student.setId(null);   // a client must never choose the database id
        return studentRepository.save(student);
    }

    public List<Student> getAll() {
        return studentRepository.findAll();
    }
}

Step 8: Create Controller class

package com.example.customvalidation.controller;

import com.example.customvalidation.entity.Student;
import com.example.customvalidation.service.StudentService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;
import java.util.TreeMap;

@RestController
@RequestMapping("/students")
public class StudentController {

    private final StudentService studentService;

    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    // POST /students  (@Valid runs the standard rules and our @AdmissionEligible rule)
    @PostMapping
    public ResponseEntity<Student> admit(@Valid @RequestBody Student student) {
        return ResponseEntity.status(HttpStatus.CREATED).body(studentService.admit(student));
    }

    // GET /students
    @GetMapping
    public List<Student> getAll() {
        return studentService.getAll();
    }

    // Validation failed -> 400 with { field: message }
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidation(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new TreeMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
                errors.merge(error.getField(), error.getDefaultMessage(),
                        (first, second) -> first + "; " + second));
        return ResponseEntity.badRequest().body(errors);
    }
}

Step 9: Main Application

package com.example.customvalidation;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StudentCustomValidationApplication {

    public static void main(String[] args) {
        SpringApplication.run(StudentCustomValidationApplication.class, args);
    }
}

Step 10: Run the StudentCustomValidationApplication

Scroll to Top