Create the following folder structure:

pom.xml: (if using Gradle, replace pom.xml with build.gradle)
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>net.maxjava</groupId> <artifactId>SpringWebServiceValidation</artifactId> <version>1.0</version> <packaging>war</packaging> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>6.1.6</version> </dependency> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>8.0.1.Final</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.4</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <source>17</source> <target>17</target> </configuration> </plugin> </plugins> </build> </project>
plugins { id 'java' id 'war' } java { sourceCompatibility = 17 targetCompatibility = 17 } group 'net.maxjava' version '1.0' repositories { mavenCentral() } dependencies { implementation group: 'org.springframework', name: 'spring-webmvc', version: '6.1.6' implementation group: 'org.hibernate', name: 'hibernate-validator', version: '8.0.1.Final' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.15.4' compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.30' annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.30' }
Some notes about pom.xml:
- spring-webmvc for necessary Spring REST controllers and its dependencies
- lombok to remove boilerplate code like get and set
- hibernate-validator is required for implementation of the validation
- jackson-databind is for the java object serialization and deserialization
- maven-compiler-plugin is for compiling the code using source version 17 and target version 17
- A war artifact will be created for deployment to servlet containers
Some notes about build.gradle:
- spring-webmvc for necessary Spring REST controllers and its dependencies
- lombok to remove boilerplate code like get and set
- hibernate-validator is required for implementation of the validation
- jackson-databind is for the java object serialization and deserialization
- sourceCompatibility and targetCompatibility is for compiling the code using source version 17 and target version 17
- A war artifact will be created for deployment to servlet containers
web.xml:
<web-app> <display-name>Spring Web Service with validation</display-name> <servlet> <servlet-name>SpringWebServiceValidation</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/myApplicationContext.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>SpringWebServiceValidation</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
Some notes about web.xml:
- org.springframework.web.servlet.DispatcherServlet is the servlet class for Spring
- /WEB-INF/myApplicationContext.xml is the location of the Spring context configuration file
myApplicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <context:component-scan base-package="controller,exception"/> <mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/> </mvc:message-converters> </mvc:annotation-driven> </beans>
Some notes about myApplicationContext.xml:
- <context:component-scan base-package="controller,exception"/> is for Spring to look for classes in specific packages to create beans
- <mvc:message-converters> is to specify library for json serialization and deserialization
MyController.java:
package controller; import model.Customer; 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 jakarta.validation.Valid; @RestController @RequestMapping("/myapp") public class MyController { @PostMapping("/customer") public void postCustomer(@Valid @RequestBody Customer customer) { System.out.println("Customer name: " + customer.getFirstName()); } }
Some notes about MyController.java:
- It is a normal REST controller for taking POST request with "Customer" as the body
- Put the annotation @Valid in front of the parameter for validation
Customer.java:
package model; import lombok.Data; import validator.NoDuplication; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import java.util.List; @Data public class Customer { @NotNull(message = "Please provide a lastName") private String lastName; private String firstName; @Email(message = "email is not in correct format") private String email; @Positive(message = "age must be positive") private Integer age; @Max(value = 5, message = "Maximum number of reference is 5") private Integer reference; @NoDuplication(message = "Duplicated hobbies are found") private List<String> hobbies; }
Some notes about Customer.java:
- This is the Customer class used in the request body of the postCustomer method in MyController
- Except for @NoDuplication, which is a customized constraint, other annotation is built-in constraints. Please refer to here for all the available constraints and their meanings
- @NoDuplication is a customized constraint on validation, see below for explanation
ErrorResponse.java:
package model; import lombok.Data; @Data public class ErrorResponse { private int errorCode; private String errorMessage; }
Some notes about ErrorResponse.java:
- This is the ErrorResponse if there is constraint violation
NoDuplication.java:
package validator; import jakarta.validation.Constraint; import jakarta.validation.Payload; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target(FIELD) @Retention(RUNTIME) @Constraint(validatedBy = NoDuplicationValidator.class) public @interface NoDuplication { String message() default "Duplications are found"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Some notes about NoDuplication.java:
- Create the annotation NoDuplication with @interface
- Specify the customized NoDuplicationValidator (see below) as the validator
- It requires the message for the default failed validation message
- groups and payload just use default value
NoDuplicationValidator.java:
package validator; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import java.util.HashSet; import java.util.List; import java.util.Set; public class NoDuplicationValidator implements ConstraintValidator<NoDuplication, List<String>> { @Override public void initialize(NoDuplication constraintAnnotation) { } @Override public boolean isValid(List<String> list, ConstraintValidatorContext constraintValidatorContext) { boolean valid = true; if (list != null) { Set<String> set = new HashSet<>(list); if (set.size() != list.size()) { valid = false; } } return valid; } }
Some notes about NoDuplicationValidator.java:
- It implements the interface ConstraintValidator, with NoDuplication as the annotation type and List<String> as the field type
- Implementing the isValid method with customized logic to determine whether the value is valid or not. If the value is valid then return true, else return false
MyExceptionHandler.java:
package exception; import model.ErrorResponse; 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.RestControllerAdvice; @RestControllerAdvice public class MyExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException methodArgumentNotValidException) { ErrorResponse errorResponse = new ErrorResponse(); errorResponse.setErrorCode(HttpStatus.BAD_REQUEST.value()); errorResponse.setErrorMessage(methodArgumentNotValidException.getBindingResult().getFieldError().getDefaultMessage()); ResponseEntity<ErrorResponse> responseEntity = new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); return responseEntity; } }
Some notes about MyExceptionHandler.java:
- Failed validation will throw an uncaught exception all the way escalating to the servlet container, which will display as an unfriendly response back to the client. So an exception handler is used to capture specific exception and return with a nicely formatted response
- Mark @RestControllerAdvice in a class to capture exception globally and in a method define the annotation @ExceptionHandler with MethodArgumentNotValidException.class (which is the exception that will be thrown for failed validation)
- In the method define customized response structure which is ErrorResponse in this case and return an instance of ResponseEntity
- The failed validation message is retrieved from the method argument methodArgumentNotValidException
Sample requests and responses(assume the war file is deployed to <CONTEXT_PATH>):
- Send a valid record:
Response status: 200 OKcurl --request POST '<CONTEXT_PATH>myapp/customer' --header 'Content-Type: application/json' --data-raw ' { "firstName":"Sam", "lastName":"Johnson", "age":30, "email":"hithere@maxjava.net", "reference":1, "hobbies":["fishing","bowling"] }'
- Send a record which "lastName" is missing:
Response status: 400 Bad Requestcurl --request POST '<CONTEXT_PATH>/myapp/customer' --header 'Content-Type: application/json' --data-raw ' { "firstName":"Sam", "age":30, "email":"hithere@maxjava.net", "reference":1, "hobbies":["fishing","bowling"] }'
body:{ "errorCode": 400, "errorMessage": "Please provide a lastName" } - Send a record which "age" is negative:
Response status: 400 Bad Requestcurl --request POST '<CONTEXT_PATH>/myapp/customer' --header 'Content-Type: application/json' --data-raw ' { "firstName":"Sam", "lastName":"Johnson", "age":-10, "email":"hithere@maxjava.net", "reference":1, "hobbies":["fishing","bowling"] }'
body:{ "errorCode": 400, "errorMessage": "age must be positive" } - Send a record which "email" is not in email format:
Response status: 400 Bad Requestcurl --request POST '<CONTEXT_PATH>/myapp/customer' --header 'Content-Type: application/json' --data-raw ' { "firstName":"Sam", "lastName":"Johnson", "age":10, "email":"hithere.maxjava.net", "reference":1, "hobbies":["fishing","bowling"] }'
body:{ "errorCode": 400, "errorMessage": "email is not in correct format" } - Send a record which "hobbies" has duplicated values:
Response status: 400 Bad Requestcurl --request POST '<CONTEXT_PATH>/myapp/customer' --header 'Content-Type: application/json' --data-raw ' { "firstName":"Sam", "lastName":"Johnson", "age":30, "email":"hithere@maxjava.net", "reference":1, "hobbies":["fishing","bowling","fishing"] }'
body:{ "errorCode": 400, "errorMessage": "Duplicated hobbies are found" }