Post

Validar requests con Spring Validation

Veremos varias anotaciones de spring-boot-starter-validation y como validar la informaci贸n que llega a nuestros servicios antes de ejecutar procesos 馃懡

Anotaci贸nDescripci贸n
@NotNullVerifica que un objeto no sea nulo
@NotEmptyVerifica que el objeto no sea nulo y no est茅 vac铆o
@NotBlankVerifica que un objeto no sea nulo y no contenga solo caracteres en blanco
@SizeVerifica que un String o Iterable tenga tama帽os l铆mite
@MaxVerifica que el n煤mero sea >= (Mayor igual) al indicado
@MinVerifica que el n煤mero sea =< (Menor igual) al indicado
@PatternVerifica que el objeto coincida con un regex
@PastVerifica que la fecha sea menor a la actual
@FutureVerifica que la fecha sea mayor a la actual
@DigitsAsegura que el valor num茅rico del campo tenga un n煤mero espec铆fico de d铆gitos enteros y fraccionarios
@DecimalMinAsegura que el valor decimal sea >= (Mayor igual) al especificado.
@DecimalMaxAsegura que el valor decimal sea =< (Menor igual) al especificado.
@AssertTrueAsegura que el valor sea true.
@AssertFalseAsegura que el valor sea siempre false.
@PositiveAsegura el que valor sea positivo.
@PositiveOrZeroAsegura el que valor sea positivo o cero.
@NegativeAsegura el que valor sea negativo.
@NegativeOrZeroAsegura el que valor sea negativo o cero.
@URLAsegura que el valor sea un URL v谩lida.
@NegativeAsegura el que valor sea negativo.

Para usar estas anotaciones y validar la informaci贸n, seguiremos los siguientes pasos:

  • Agregar la dependencia spring-boot-starter-validation en nuestro pom.xml
1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • Debemos anotar con @Valid el dto.
1
2
3
4
5
@DeleteMapping
@ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<Void> delete(@RequestBody @Valid final QuotaCodRequestDto quotaCodRequestDto) {
    return this.quotaService.delete(quotaCodRequestDto);
}
  • Si queremos validar @PathVariable o @RequestParam, debemos agregar la anotaci贸n @Validated en nuestro controlador
1
2
3
4
@Validated
public class QuotaController {
  ...
}

Para cada anotaci贸n podemos indicar un mensaje de error en caso de que la validaci贸n no se cumpla

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public record QuotaFilterRequestDto(
        @Size(max = 50, message = "El campo description tiene un m\u00e1ximo de {max} caracteres")
        String description,

        Boolean finished,

        @NotNull(message = "El campo pago es obligatorio")
        @Min(value = 0, message = "El campo pageNo tiene un m铆nimo de {value}")
        Integer pageNo,

        @NotNull(message = "El campo pageSize es obligatorio")
        @Min(value = 1, message = "El campo pageSize tiene un m铆nimo de {value}")
        @Max(value = 10, message = "El campo pageSize tiene un m谩ximo de {value}")
        Integer pageSize
) {
}

Podemos usar placeholders {} en los mensajes de error, esto facilita el que si tenemos que cambiar alg煤n valor de la validaci贸n, no nos preocupemos por cambiar en el mensaje tambi茅n. 馃


Si queremos capturar el mensaje de error para devolverlo como respuesta en el api, lo haremos con un @RestControllerAdvice

1
2
public record ErrorMessageResponseDto(String message, Instant timestamp) {
}

Usaremos este dto para devolver informaci贸n sobre los errores de validaci贸n.

  • Capturar errores de @RequestBody + @Valid
1
2
3
4
5
6
7
8
9
10
@RestControllerAdvice
public class GlobalExceptionHandler {
  @ExceptionHandler(MethodArgumentNotValidException.class)
  public ResponseEntity<ErrorMessageResponseDto> handleMethodValidationExceptions(final MethodArgumentNotValidException ex) {
    final String errorMessage = ex.getBindingResult().getAllErrors().stream().findFirst()
      .map(DefaultMessageSourceResolvable::getDefaultMessage).orElse("Validation error unexpected");

    return new ResponseEntity<>(new ErrorMessageResponseDto(errorMessage, Instant.now()), HttpStatus.UNPROCESSABLE_ENTITY);
  }
}
  • Capturar errores de @RequestParam, @PathVariable + @Validated
1
2
3
4
5
6
7
8
9
10
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ErrorMessageResponseDto> handleConstraintViolationExceptions(final ConstraintViolationException ex) {
        final String errorMessage = ex.getConstraintViolations().stream().findFirst()
                .map(ConstraintViolation::getMessage).orElse("Validation error unexpected");

        return new ResponseEntity<>(new ErrorMessageResponseDto(errorMessage, Instant.now()), HttpStatus.UNPROCESSABLE_ENTITY);
    }
}

Al validar @RequestBody + @Valid la excepci贸n es del tipo MethodArgumentNotValidException. Y si validamos @RequestParam, @PathVariable + @Validated la excepci贸n es ConstraintViolationException 馃挕


Por defecto @Valid 煤nicamente verifica y valida los campos de primer nivel del dto.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class AdminRegisterRequestDto implements Serializable {
  @NotBlank(message = "El nombre es obligatorio")
  @Pattern(regexp = "^[a-zA-Z\\s]{5,100}$", message = "El nombre solo permite letras")
  private String name;

  @NotBlank(message = "El apellido es obligatorio")
  @Pattern(regexp = "^[a-zA-Z\\s]{5,100}$", message = "El apellido solo permite letras")
  private String lastName;

  @NotNull(message = "Los datos de usuario son obligatorios")
  private UsuarioRegisterDto user;
}

Por ejemplo, aqu铆 煤nicamente se validar谩n los campos name y lastName, si el dto UsuarioRegisterDto tambi茅n tiene validaciones no se tomar谩n en cuenta.

Para validar dto鈥檚 anidados debemos anotarlos con @Valid, esto garantiza que se validen los campos de ese dto.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class AdminRegisterRequestDto implements Serializable {
  @NotBlank(message = "El nombre es obligatorio")
  @Pattern(regexp = "^[a-zA-Z\\s]{5,100}$", message = "El nombre solo permite letras")
  private String name;

  @NotBlank(message = "El apellido es obligatorio")
  @Pattern(regexp = "^[a-zA-Z\\s]{5,100}$", message = "El apellido solo permite letras")
  private String lastName;

  @Valid
  @NotNull(message = "Los datos de usuario son obligatorios")
  private UsuarioRegisterDto user;
}

驴Por qu茅 validar la informaci贸n de nuestros servicios? 馃

  • Garantiza que se trabaje con informaci贸n limpia, evitando errores inesperados.
  • Optimiza el uso de recursos, ya que si los datos no pasan la validaci贸n nunca llega a ejecutar l贸gica interna (Consultar base de datos, llamar a servicios terceros, etc.)
  • Informamos los errores de manera detallada siguiendo est谩ndares y buenas pr谩cticas.

Observaciones

  • No todas las anotaciones se pueden usar con todos los tipos de datos. Por ejemplo si tratamos de validar con @Past un Integer obtendremos una excepci贸n
1
jakarta.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'jakarta.validation.constraints.Past' validating type 'java.lang.Integer'. Check configuration for 'pageNo'
This post is licensed under CC BY 4.0 by the author.