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

For our ProfileDto class, define a custom annotation that verifies if the URL is valid. 
Annotations don't include any business logic; they are only the code's metadata. They can only provide information about the attribute (class/method/package/field) on which it's defined.

Lets create our @ValidURL 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

Add validation logic for url field from ProfileDto.java. 

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

Add @ValidURL annotation to ProfileDto.java dto class.

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

When the validation fails, Spring will throw an exception of type MethodArgumentNotValidException and we need to handle it in RestExceptionHandler class.

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.

Source code - click here!

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java Stream API - How to convert List of objects to another List of objects using Java streams?

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Java, Spring Boot Mini Project - Library Management System - Download

ReactJS, Spring Boot JWT Authentication Example

Top 5 Java ORM tools - 2024

Java - Blowfish Encryption and decryption Example

Spring boot video streaming example-HTML5

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete