Request Validation in Spring Boot

Returning the consistent and proper response codes and body signifies matured API design (Uniform Interface). In this Spring boot validation tutorial, we will learn to validate the incoming requests to the API controllers using JSR-380 provided validation annotations.

1. JSR-380 Spec and Hibernate Validator API

Bean Validation 2.0 (JSR 380) is the specification of the Java API for JavaBean validation that provides a class-level constraint declaration and validation facility using annotations. The constraint annotations are applied on types, methods, fields or other constraint annotations in case of composite annotations.

Bean Validation 2.0 is the successor of Bean Validation 1.0 (JSR 308) that is primarily focused on embracing Java 8 features.

Hibernate validator is the certified reference implementation of JSR 380 that provides many improvements over version 1.x, such as:

  • support for java.util.Optional
  • support for custom container types
  • support for the new date/time data types (JSR 310) for @Past and @Future
  • new built-in constraints such as @Email, @NotEmpty
  • leverage the JDK 8 new features

2. Dependency

Include spring-boot-starter-validation dependency in the project.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

The starter dependency transitively includes the following required dependencies.

  • org.apache.tomcat.embed:tomcat-embed-el
  • org.hibernate.validator:hibernate-validator
  • com.fasterxml:classmate
  • jakarta.validation:jakarta.validation-api
  • org.jboss.logging:jboss-logging

3. Applying Constraint Annotations on Resource Model

The hibernate validator API provides a long list of constraint annotations that we can apply to the domain classes. Let us see how to use these annotations with an example.

public class User 
{
  private Long id;

  @NotBlank(message = "First name is mandatory")
  private String firstName;

  @NotBlank(message = "Last name is mandatory")
  private String lastName;

  @NotBlank(message = "Email is mandatory")
  @Pattern(regexp=".+@.+\\.[a-z]+")
  private String email;
}

In the above User class, we have applied the following constraints:

  • First name is mandatory.
  • Last name is mandatory.
  • Email is mandatory.
  • Email must follow a regex pattern.

4. Enforce Validation of API Requests

Only applying the validation annotations on the fields in the domain class is not enough. To enforce the validation process to trigger, we must add the @Valid annotation on the model class in the request handler method.

In the given examples, we have applied the @Valid annotation on the User model.

@PostMapping
public ResponseEntity<?> create(@Valid @RequestBody User newUser) {
	//...
}

@PutMapping("/{id}")
public EntityModel<User> update(@Valid @RequestBody User newUser,
 	@PathVariable Long id) {
	//...
}

The above changes will throw MethodArgumentNotValidException exception in runtime when we send an incomplete request body, and the constraint validation fails. By default, spring boot translates the exception to response code 400 Bad Request.

The Spring boot provided default error message will be:

{
    "timestamp": "2022-01-14T20:17:33.601+00:00",
    "status": 400,
    "error": "Bad Request",
    "trace": "...a long trace...",
    "message": "Validation failed for object='user'. Error count: 1",
    "errors": [
        {
            "codes": [
                "NotBlank.user.firstName",
                "NotBlank.firstName",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.firstName",
                        "firstName"
                    ],
                    "arguments": null,
                    "defaultMessage": "firstName",
                    "code": "firstName"
                }
            ],
            "defaultMessage": "First name is mandatory",
            "objectName": "user",
            "field": "firstName",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotBlank"
        }
    ],
    "path": "/users"
}

5. Sending Custom Validation Response

The default response is correct, but it exposes a lot of technical information that may not be useful for non-technical consumers of the API. Also, the default response does not contain any clearly worded error message that we can directly show on the client UI.

A better approach would be to return a response which API consumers can use directly. For that, we need to write an exception handler method, catch the MethodArgumentNotValidException. Then we need to build the required message and return it to the API consumers with the appropriate response code.

A similar exception handler using @ControllerAdvice is given below. Change this implementation as per your needs.

@ControllerAdvice
public class GlobalExceptionHandler {

	private static final String REQUEST_VALIDATION_ERRORS 
		= "Request validation errors";

	@ResponseStatus(HttpStatus.BAD_REQUEST)
	@ExceptionHandler(MethodArgumentNotValidException.class)
	public ResponseEntity<ErrorResponse> 
		handleValidationExceptions(MethodArgumentNotValidException ex) {

		List<String> details = new ArrayList<String>();
		for (ObjectError error : ex.getBindingResult().getAllErrors()) {
			details.add(error.getDefaultMessage());
		}
		ErrorResponse error = new ErrorResponse(REQUEST_VALIDATION_ERRORS, details);
		return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
	}
}

After placing the above exception handler method, we get a more helpful error response.

{
    "timestamp": "2022-01-15T02:11:40.7922705",
    "message": "Request validation errors",
    "details": [
        "First name is mandatory"
    ]
}

6. Localized Error Messages

Spring boot supports localized messages and, by default, looks for the presence of a messages.properties resource bundle at the root of the classpath. Note that when Spring boot finds the messages.properties file, it supplies auto-configured MessageSource bean.

first.name.missing=First name is mandatory
last.name.missing=Last name is mandatory
email.missing=Email is mandatory
email.format.error=Email format is not correct

Apart from the above default properties file (mandatory), we can also define other language-specific properties files.

Now we can use the property keys in the annotated fields.

public class User 
{
  private Long id;

  @NotBlank(message = "{first.name.missing}")
  private String firstName;

  @NotBlank(message = "{last.name.missing}")
  private String lastName;

  @NotBlank(message = "{email.missing}")
  @Pattern(regexp=".+@.+\\.[a-z]+", message = "{email.format.error}")
  private String email;
}

When we rerun the application, the application will pick the messages from the correct resource bundle.

7. Hibernate Validator Annotations List

Hibernate Validator comprises all JSR-380 annotations as well as its own custom annotations. To read about all annotations in-depth, please refer to official docs.

7.1. Jakarta Validation API Provided Constraints

These constraints are part of standard bean validation 2.0.

ConstraintDescription
@AssertTrueChecks if a boolean value is true.
@AssertFalse Checks if a boolean value is false.
@DecimalMaxChecks if a given number is lower or equal to a specified value.
@DecimalMin Checks if a given number is higher or equal to a specified value.
@Digits(integer=, fraction=)Checks if a number has up to integer digits and fraction fractional digits.
@EmailChecks if a String is a valid email address.
@MaxChecks if a number is less than or equal to the specified maximum.
@MinChecks if a number is higher than or equal to the specified minimum
@NotBlankChecks if a String is not null and the trimmed length exceeds 0.
@NotEmptyChecks if a String is not null nor empty.
@NullChecks if an object is null
@NotNullChecks if an object is not null
@Pattern(regex=, flags=)Checks if a string matches the regular expression regex considering the given flag match
@Size(min=, max=)Checks if a number is between min (exclusive) and max (inclusive)
@Positive Checks if a number is strictly positive. Zero values are considered invalid.
@PositiveOrZero Checks if a number is positive or zero.
@NegativeChecks if a number is strictly negative. Zero values are considered invalid.
@NegativeOrZeroChecks if a number is negative or zero.
@FutureChecks if a date is in the future.
@FutureOrPresentChecks if a date is in the present or in the future.
@Past Checks if a date is in the past.
@PastOrPresentChecks if a date is in the past or in the present.

7.2. Hibernate Validator Provided Constraints

Apart from supporting standard constraints, hibernate validator provides a few of its own custom constraints listed below.

ConstraintDescription
@CreditCardNumberChecks if a string passes the Luhn checksum test. Note, this validation aims to check for user mistakes, not credit card validity!
@CurrencyChecks if a javax.money.MonetaryAmount instance is part of the specified currency units.
@DurationMaxChecks if java.time.Duration instance is not greater than the specified value
@DurationMinChecks if java.time.Duration instance is not lower than the specified value
@EANChecks if a string is a valid EAN barcode. The default is EAN-13.
@ISBNChecks if a string is a valid ISBN.
@LengthValidates if a string length is between min and max included.
@CodePointLengthChecks if code point length of a string is between a max and min value
@LuhnCheckChecks if the digits of a value pass the Luhn checksum algorithm
@Mod10CheckChecks if the digits of a value pass the generic mod 10 checksum algorithm
@Mod11CheckChecks if the digits of a value pass the generic mod 11 checksum algorithm
@NormalizedChecks if a string is normalized according to the given form.
@RangeChecks if a number lies between (inclusive) the specified minimum and maximum.
@ScriptAssertChecks if a script can successfully be evaluated against the field value. 
@UniqueElementsChecks if a collection only contains unique elements.
@URLChecks if a string is a valid URL according to RFC2396.

8. Conclusion

In this Spring boot request validation example, we learned to configure the hibernate validator API to validate the incoming API requests. We also learned to apply the constraint validations on the domain classes and to trigger the validation process.

We also saw to use the language-specific validation messages using the resource bundles.

You can check out the source code on Github.

Leave a Comment