Spring boot 3 with Java data validation

Nithidol Vacharotayan
6 min readApr 26, 2024

--

Image by vectorjuice on Freepik

Data validation helps screen invalid client data and makes code simple to maintain. Jakarta validation provides various annotations for validating data, such as NotEmpty or Digits. These annotations provide flexible parameters for custom messages. The developer can create custom validation annotations for implementation with Jakarta validation.

Scenario

The developer team create restful web services for creating user profiles.

Discussion

The developer team decided to use Jakarta validation for screen data from the request body to prevent invalid data, such as invalid email patterns or empty required values.

Implementation

The developer team created RESTful Web Services using Spring Boot 3 and integrated them with Java data validation.

Create RESTful Web Services using Spring Boot 3

  1. Create controller class UserProfilesController
import com.example.demo.bean.UserProfileBean;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserProfilesController {
@PostMapping(path = "/user-profiles", produces = "application/json")
public ResponseEntity<String> create(@Valid @RequestBody UserProfileBean userProfile) {
//custom process
return new ResponseEntity<>("created", HttpStatus.CREATED);
}

}

Valid annotation is for this method using Java data validation.
RequestBody annotation using UserProfileBean as a request body.

2. Create a Java bean for the request body.

import com.example.demo.validator.PersonIdValidate;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Data
public class UserProfileBean {

@PersonIdValidate
private String personalId;

@NotEmpty
private String firstName;

@NotEmpty
private String lastName;

@Email
private String email;

@Min(0)
private long age;

public UserProfileBean(String personalId, String firstName, String lastName, int age, String email){
this.personalId = personalId;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.age = age;
}
}

This class contains Java data validation for screening invalid data from the clients. PersonIdValidate annotation is a custom validation.

3. Create custom validation for validating personal ID.

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
class PersonIdValidator implements ConstraintValidator<PersonIdValidate, String> {

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return validatePersonId(value);
}

public boolean validatePersonId(String pid) {
//custom validation personal ID
return !pid.isEmpty();
}
}
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ FIELD })
@Retention(RUNTIME)
@Constraint(validatedBy = PersonIdValidator.class)
@Documented
public @interface PersonIdValidate {

String message() default "invalid personal ID or personal ID is empty.";

Class<?>[] groups() default { };

Class<? extends Payload>[] payload() default { };

}

4. Create an exception handler for MethodArgumentNotValidException

import com.example.demo.bean.CommonResponseBean;
import com.example.demo.bean.ErrorBean;
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.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.ArrayList;
import java.util.Date;

@RestControllerAdvice
public class CommonExceptionHandlers {

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<CommonResponseBean> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
// Customize the response entity
CommonResponseBean res = new CommonResponseBean();
res.setTimestamp(new Date().getTime());
res.setErrors(new ArrayList<>());
res.getErrors().add(new ErrorBean("E0002",ex.getMessage()));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(res);
}
}

Learn more about Rest Controller Advice annotation.
https://medium.com/@balloon.helps/spring-boot-3-exception-handlers-with-rest-controller-advice-annotation-2bea56f9422e

Test case scenario

Test case: Send a request to path /user-profiles.

Test result: web services response HTTP status 201 and request body “created”.

Test case: Send a request to path /user-profiles with an empty personal ID.

Test result: Web services respond to HTTP status 400 and request body with error information, not an empty personal ID.

Test case: Send a request to path /user-profiles with an empty first name.

Test result: Web services respond to HTTP status 400 and request body with error information, not an empty first name.

Test case: Send a request to path /user-profiles with an invalid email pattern.

Test result: Web services respond to HTTP status 400 and request body with error information and invalid email.

The error message from “MethodArgumentNotValidException” is complex for the client to read. The developer must customise the message error from “MethodArgumentNotValidException” in the exception handler class.

Custom an error message from “MethodArgumentNotValidException.”

The developer can customize the rest of the controller advice annotation in the CommonExceptionHandlers class.

@RestControllerAdvice
public class CommonExceptionHandlers {

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<CommonResponseBean> methodArgumentNotValidException(MethodArgumentNotValidException ex) {
List<ErrorBean> message = new ArrayList<>();
// Get field name
ex.getFieldErrors().forEach(error -> {
if (null == error.getDefaultMessage()) return;
message.add(new ErrorBean("E0001",error.getField() +", "+ error.getDefaultMessage()));
}
);
CommonResponseBean res = new CommonResponseBean();
res.setTimestamp(new Date().getTime());
res.setErrors(message);
return new ResponseEntity<>(res, HttpStatus.BAD_REQUEST);
}
}

The developer can create a custom message by reading each error message in the MethodArgumentNotValidException class and adding it to the list of error beans. Each validation can have more than one field. Java data validation screens data in all fields in the process.

Custom error message Java data validation in UserProfileBean

The developer can use the default error message from Java data validation or a custom error message. This custom can integrate with message properties to support multiple languages.

@Getter
@Setter
public class UserProfileBean {

@PersonIdValidate
private String personalId;

@NotEmpty(message = "This is a custom message.")
private String firstName;

@NotEmpty
private String lastName;

@Email
private String email;

@Min(0)
private long age;

public UserProfileBean(String personalId, String firstName, String lastName, int age, String email){
this.personalId = personalId;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.age = age;
}
}

Test case scenario

Test case: Send a request to path /user profiles with an empty personal ID, first name, and an invalid email pattern.

Test result: Web services respond to HTTP status 400 and request body with multiple field error messages.

Custom error messages support multiple languages.

The developer custom error message for support multiple languages by following this step.

Create messages_fr.properties to support the French language.

not.empty=pas vide

Create messages_ja.properties to support the Japanese language.

not.empty=空ではない

Create MessageUtils to manipulate message codes from the error messages.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;

import java.util.Locale;

@Component
public class MessageUtils {

private final MessageSource messageSource;

public MessageUtils(MessageSource messageSource) {
this.messageSource = messageSource;
}

public String getMessage(String code, Object[] args, Locale locale) {
return messageSource.getMessage(code, args, locale);
}
}

Modify the CommonExceptionHandlers class to support accepting language from the request header and get messages from the message code.

@RestControllerAdvice
public class CommonExceptionHandlers {

private final MessageUtils messageUtils;

private Locale locale;

@Autowired
public CommonExceptionHandlers(MessageUtils messageUtils) {
this.messageUtils = messageUtils;
}

@ModelAttribute
public void addAcceptLanguageHeader(@RequestHeader("Accept-Language") String acceptLanguage, HttpServletRequest request) {
// Parse the Accept-Language header to get the preferred locale
this.locale = Locale.lookup(Locale.LanguageRange.parse(acceptLanguage),Arrays.asList(Locale.FRENCH, Locale.JAPANESE, Locale.getDefault()));
}


@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<CommonResponseBean> methodArgumentNotValidException(MethodArgumentNotValidException ex) {
List<ErrorBean> message = new ArrayList<>();
// Get field name
ex.getFieldErrors().forEach(error -> {
if (null == error.getDefaultMessage()) return;
message.add(new ErrorBean("E0001", error.getField() + ", " + messageUtils.getMessage(error.getDefaultMessage(), new Object[]{}, locale)));
}
);
CommonResponseBean res = new CommonResponseBean();
res.setTimestamp(new Date().getTime());
res.setErrors(message);
return new ResponseEntity<>(res, HttpStatus.BAD_REQUEST);
}
}

Test case scenario

Test case: Send a request to path /user-profiles with an empty first name and accept-language header value “fr-FR”.

Test result: Web services respond to HTTP status 400 and the request body with error messages in French.

Test case: Send a request to path /user profiles with an empty first name and the accept-language header value “ja-JP.”

Test result: Web services respond to HTTP status 400 and the request body with error messages in Japanese.

Finally, the developer should know all annotations provided by Java data validation to prevent wasting time creating custom validation that redundant Java data validation has provided.

Learn more about constraints in Jakarta Bean Validation.
https://jakarta.ee/specifications/bean-validation/3.0/apidocs/

Thank you for reading.

--

--

Nithidol Vacharotayan

Programming enthusiast with years of experience loves sharing knowledge with others and exploring new technologies together!