Spring Boot - DTO Validation with a Custom Annotation
In this section, we will learn how to create our own custom validator annotation in a simple Spring Boot application.
*Note: This is solely a demonstration of DTO Validation with a Custom Annotation.
Creating a spring boot application
First, open the Spring initializr https://start.spring.io
Then, Provide the Group and Artifact name. We have provided Group name com.knf.dev.demo and Artifact spring-boot-dto-validation-custom-annotation. Here I selected the Maven project - language Java 17 - Spring Boot 3.3.0, Spring Web, and Validation.
Then, click on the Generate button. When we click on the Generate button, it starts packing the project in a .zip(spring-boot-dto-validation-custom-annotation) file and downloads the project. Then, Extract the Zip file.
Then, import the project on your favourite IDE.
Final Project Directory
Complete 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.3.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>spring-boot-dto-validation-custom-annotation</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-dto-validation-custom-annotation</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Create a Custom Annotation
package com.knf.dev.demo.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Constraint(validatedBy = URLValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValidURL {
String message() default "URL is invalid";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
As you can see, the @interface keyword is used to construct annotations. Before moving forward, let's look at a few of the terms and make sure we fully grasp them:
- @Documented: A marker annotations which tell whether to add Annotation in Javadocs or not.
- @Constraint: Marks an annotation as being a Bean Validation Constraint. The element validatedBy specifies the classes implementing the constraint. We will create the URLValidator class for the same.
- @Target: You can put the annotation wherever if you don't specify it. At the moment, our annotation can be overlaid over an instance variable.
- @Retention: During runtime, our program retains and makes accessible the annotations annotated with the RUNTIME retention policy.
Create a URL Validator
package com.knf.dev.demo.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
public class URLValidator
implements ConstraintValidator<ValidURL, String> {
@Override
public boolean isValid(String url, ConstraintValidatorContext context) {
try {
new URL(url).toURI();
} catch (MalformedURLException | URISyntaxException e) {
return false;
}
return true;
}
}
ProfileDto
package com.knf.dev.demo.dto;
import com.knf.dev.demo.validation.ValidURL;
public class ProfileDto {
private String userName;
@ValidURL
private String url;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
Create Exception Handler
package com.knf.dev.demo.exception;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, List<String>>> process
(MethodArgumentNotValidException ex, HttpServletRequest request) {
List<String> errors = new ArrayList<>();
ex.getAllErrors().forEach(err -> errors.add(err.getDefaultMessage()));
Map<String, List<String>> result = new HashMap<>();
result.put("errors", errors);
return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST);
}
}
Create a Controller
package com.knf.dev.demo.controller;
import com.knf.dev.demo.dto.ProfileDto;
import jakarta.validation.Valid;
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;
@RestController
@RequestMapping("/test/validation")
public class UserController {
@PostMapping
public String test(@Valid @RequestBody ProfileDto profileDto) {
return "Done";
}
}
Note that we enable validation on Spring Rest Controller by adding @Valid annotation in addition to @RequestBody in the test() method.
Run the application - Application.java
package com.knf.dev.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Test
Send an invalid URL to verify the validation works:
We can create several custom annotations in this manner for validation needs.