Validation (Spring Boot)

Introduction

Let's see a Spring Boot 2.2.6.RELEASE application using both Bean Validation (JSR 380) and custom validations.


References

https://www.logicbig.com/tutorials/java-ee-tutorial/bean-validation/predefined_constraints.html

https://docs.jboss.org/hibernate/validator/4.1/reference/en-US/html/validator-customconstraints.html#validator-customconstraints-using

https://mkyong.com/spring-boot/spring-rest-validation-example/

https://www.baeldung.com/spring-boot-internationalization




POM configuration (pom.xml)

    <dependency>

        <!-- Only explicitly needed if not using spring-boot-starter-web -->

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-validation</artifactId>

    </dependency>    

import: jakarta vs javax

For annotations, use the proper import:

jakarta.validation.constraints.Email       javax.validation.constraints.Email

jakarta.validation.constraints.NotBlank    javax.validation.constraints.NotBlank

jakarta.validation.constraints.NotNull     javax.validation.constraints.NotNull

jakarta.validation.constraints.Pattern     javax.validation.constraints.Pattern

jakarta.validation.constraints.Positive    javax.validation.constraints.Positive

jakarta.validation.constraints.Size        javax.validation.constraints.Size


REST operation parameter path or query string

Decorate the path or query string parameter with validation contraint, eg: @NotBlank

@Parameter(description = "Language", required = true, example = "ca") @RequestParam(

          name = "lang", required = true) @NotBlank(message = "{error.lang.notblank}") String lang,

REST operation parameter request body

Decorate the RestController with @Validated

@Validated

@RestController

public class MyController {

Decorate the body parameter with @Valid

@RequestBody(required = true) @Valid SampleValDto in

where SampleValDto is:

@Data

public class SampleValDto {

  /** Must be a valid ISO 639-1 code */

  @Schema(description = "Lang ISO-639-1", required = false)

  @NotBlank(message = "{error.lang.notblank}")

  @Nullable

  private String langIso639p1Code;

}


Internationalization of constrain messages

Spring Boot replaces the message key of the constraint by the corresponding message in the user language.

Find in this site how to configure localized messages at page "MessageSource (Spring Boot)":

https://sites.google.com/site/pawneecity/sprint-boot/messagesource-spring-boot


For this sample, the following file resources have been used:

# Errors

error.lang.notblank=Language code must not be null and must contain at least one non-whitespace character


# Validation constraints (the default resource key is the fully qualified name of the annotation class concat. to .message)

edu.cou.app.validator.LangIso639p1App.message=Unknown language ISO 639-1 code

javax.validation.constraints.NotBlank.message=Value must not be null and must contain at least one non-whitespace character

# Errors

error.lang.notblank=Kod języka nie może mieć wartości null i musi zawierać co najmniej jeden znak spacji


# Validation constraints (the default resource key is the fully qualified name of the annotation class concat. to .message)

edu.cou.app.validator.LangIso639p1App.message=Nieznany kod ISO 639-1

javax.validation.constraints.NotBlank.message=Nie może mieć wartości null i musi zawierać co najmniej jeden znak spacji



Override handling of exception MethodArgumentNotValidException

When the constraint fails, it throws this exception.

In order to provide the invoker with the proper message, ensure you custom exception handler overrides the default response behaviour (status 400 without body).

Find in this site how to override the method handleMethodArgumentNotValid at page "Error handling (Spring Boot)":

https://sites.google.com/site/pawneecity/sprint-boot/error-handling-rest-spring-boot



Custom validator

Let's create a custom annotation constraint for validating an ISO 639-1 code.

This custom annotation, @LangIso639p1App, can be used at a parameter or at an attribute. 

For demonstrating purposes, code assumes as valid only three codes: en, es, pl.


The annotation

package edu.cou.app.validator;


import javax.validation.Constraint;

import javax.validation.Payload;

import java.lang.annotation.Documented;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.Target;


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


@Target({ ElementType.FIELD, ElementType.PARAMETER })

@Retention(RUNTIME)

@Constraint(validatedBy = LangIso639p1AppValidator.class)

@Documented

public @interface LangIso639p1App {


  /** Default resource key is the fully qualified name of the annotation class concatenated to .message */

  String message() default "{edu.cou.app.validator.LangIso639p1App.message}";


  /***/

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


  /***/

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


}


The validator

package edu.cou.app.validator;


import javax.inject.Inject;

import javax.validation.ConstraintValidator;

import javax.validation.ConstraintValidatorContext;


/**

 *

 */

public class LangIso639p1AppValidator implements ConstraintValidator<LangIso639p1App, String> {


  private final List<String> isoCodes = Arrays.asList("en", "es", "pl");


  @Override

  public boolean isValid(@Nullable String value, ConstraintValidatorContext context) {

    if (value == null){

       return true;

    }

    return isoCodes.contains(value);

  }


}

Regular expressions

AWS S3 object key name safe chars

String REGEX_OBJECT_KEY_SAFE_CHARS = "^[0-9a-zA-Z\\!\\-\\_\\.\\*'()]+$";

String REGEX_OBJECT_KEY_SAFE_CHARS_PLUS_SLASH = "^[0-9a-zA-Z\\!\\-\\_\\.\\*'()/]+$";


Email

See https://emailregex.com/


HTML 5

/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/


Java

(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])


NIF / NIE

var nifRegex = /^[0-9]{8}[TRWAGMYFPDXBNJZSQVHLCKE]$/i;

var nieRegex = /^[XYZ][0-9]{7}[TRWAGMYFPDXBNJZSQVHLCKE]$/i;


String, even it has line breaks, does not contain 'badword'

String REGEX_STR_WITHOUT_BADWORD = "(?s)^((?!badword).)*$";


Examples

@Max(27L) for CharSequence, Collection, Map, Array

@Size(min =27, max = 30)

@Pattern(regexp=String, flags=Pattern.Flag[]) for CharSequence (String)

@Pattern(regexp = "^\\d{5}$")

@Pattern(regexp = "apple|banana|melon")

@Pattern(regexp = "(?s)^((?!//).)*$")