Creando una anotación custom para validar campos en Spring Boot

Published on
🕒 4 mins
👁️ -- visitas

En este post vamos a ver cómo crear una anotación custom para validar campos en Spring Boot. En este caso, vamos a crear una anotación para validar un campo de rut.

Creando la anotación

Para crear una anotación custom en Spring Boot, debemos crear una clase que extienda de ConstraintValidator y anotarla con @Constraint. En este caso, vamos a crear una anotación para validar un campo de rut.

Rut.java
12 collapsed lines
1
package dev.fneira.customvalidation.validation;
2
3
import static java.lang.annotation.ElementType.FIELD;
4
import static java.lang.annotation.ElementType.PARAMETER;
5
6
import jakarta.validation.Constraint;
7
import jakarta.validation.Payload;
8
import java.lang.annotation.Documented;
9
import java.lang.annotation.Retention;
10
import java.lang.annotation.RetentionPolicy;
11
import java.lang.annotation.Target;
12
13
14
@Target({FIELD, PARAMETER})
15
@Retention(RetentionPolicy.RUNTIME)
16
@Documented
17
18
19
@Constraint(validatedBy = RutValidator.class)
20
public @interface Rut {
21
22
23
public String message() default "must be a well-formed RUT (e.g. 12345678-9)";
24
25
public Class<?>[] groups() default {};
26
27
public Class<? extends Payload>[] payload() default {};
28
}

En este caso, la anotación @Rut tiene un atributo message que es el mensaje que se mostrará si la validación falla. También tiene los atributos groups y payload que son necesarios para que la anotación funcione correctamente.

Creando el validador

Ahora vamos a crear la clase RutValidator que extiende de ConstraintValidator y que se encargará de validar el campo de rut.

RutValidator.java
5 collapsed lines
1
package dev.fneira.customvalidation.validation;
2
3
import jakarta.validation.ConstraintValidator;
4
import jakarta.validation.ConstraintValidatorContext;
5
6
7
public class RutValidator implements ConstraintValidator<Rut, String> {
8
9
@Override
10
public boolean isValid(final String rutValue, final ConstraintValidatorContext context) {
11
return isValidFormat(rutValue);
12
}
13
14
private boolean isValidFormat(final String rut) {
15
return rut.matches("^[0-9]+-[0-9kK]$");
16
}
17
}

En este caso, el método isValid es el que se encarga de validar el campo de rut. Si el campo es válido, el método debe retornar true, en caso contrario, debe retornar false.

Usando la anotación

Ahora que ya tenemos la anotación y el validador, podemos usarla en una clase de dominio de la siguiente manera:

ClientDTO.java
7 collapsed lines
1
package dev.fneira.customvalidation.dto;
2
3
import dev.fneira.customvalidation.validation.Rut;
4
import jakarta.validation.constraints.Email;
5
import jakarta.validation.constraints.NotBlank;
6
import java.time.LocalDate;
7
8
public class ClientDTO {
9
10
@NotBlank private String name;
11
@NotBlank private String lastName;
12
@NotBlank @Email private String email;
13
14
@Rut private String rut;
15
private LocalDate birthDate;
16
53 collapsed lines
17
// Constructors, getters and setters
18
public ClientDTO() {}
19
20
public ClientDTO(
21
final String name,
22
final String lastName,
23
final String email,
24
final String rut,
25
final LocalDate birthDate) {
26
this.name = name;
27
this.lastName = lastName;
28
this.email = email;
29
this.rut = rut;
30
this.birthDate = birthDate;
31
}
32
33
public String getName() {
34
return name;
35
}
36
37
public void setName(final String name) {
38
this.name = name;
39
}
40
41
public String getLastName() {
42
return lastName;
43
}
44
45
public void setLastName(final String lastName) {
46
this.lastName = lastName;
47
}
48
49
public String getEmail() {
50
return email;
51
}
52
53
public void setEmail(final String email) {
54
this.email = email;
55
}
56
57
public String getRut() {
58
return rut;
59
}
60
61
public void setRut(final String rut) {
62
this.rut = rut;
63
}
64
65
public LocalDate getBirthDate() {
66
return birthDate;
67
}
68
69
public void setBirthDate(final LocalDate birthDate) {
70
this.birthDate = birthDate;
71
}
72
}

En nuestro controlador, podemos usar la anotación @Valid para que Spring Boot valide automáticamente los campos de la clase ClientDTO:

ClientController.java
11 collapsed lines
1
package dev.fneira.customvalidation.controller;
2
3
import dev.fneira.customvalidation.dto.ClientDTO;
4
import dev.fneira.customvalidation.service.ClientService;
5
import jakarta.validation.Valid;
6
import org.springframework.beans.factory.annotation.Autowired;
7
import org.springframework.web.bind.annotation.PostMapping;
8
import org.springframework.web.bind.annotation.RequestBody;
9
import org.springframework.web.bind.annotation.RequestMapping;
10
import org.springframework.web.bind.annotation.RestController;
11
12
@RestController
13
@RequestMapping("/client")
14
public class ClientController {
15
16
@Autowired private ClientService clientService;
17
18
@PostMapping
19
20
public ClientDTO createClient(final @Valid @RequestBody ClientDTO clientDTO) {
21
return clientService.createClient(clientDTO);
22
}
23
}

De esta manera, cuando se intente guardar una instancia de Persona con un rut inválido, se lanzará una excepción de tipo ConstraintViolationException.

{
"timestamp": "2024-02-17T01:31:40.181+00:00",
"status": 400,
"error": "Bad Request",
"path": "/client"
}

El error no nos dice mucho, por lo que si queremos mostrar un mensaje más amigable, podemos capturar la excepción y mostrar el mensaje de error con un ControllerAdvice:

GlobalExceptionHandler.java
11 collapsed lines
1
package dev.fneira.customvalidation.controller;
2
3
import java.util.List;
4
import java.util.stream.Collectors;
5
import org.springframework.http.HttpStatus;
6
import org.springframework.validation.FieldError;
7
import org.springframework.web.bind.MethodArgumentNotValidException;
8
import org.springframework.web.bind.annotation.ExceptionHandler;
9
import org.springframework.web.bind.annotation.ResponseStatus;
10
import org.springframework.web.bind.annotation.RestControllerAdvice;
11
12
@RestControllerAdvice
13
public class GlobalExceptionHandler {
14
15
16
@ExceptionHandler(MethodArgumentNotValidException.class)
17
@ResponseStatus(HttpStatus.BAD_REQUEST)
18
public ErrorDto handleMethodArgumentNotValidException(final MethodArgumentNotValidException ex) {
19
20
return new ErrorDto(
21
"Validation error",
22
ex.getBindingResult().getFieldErrors().stream()
23
.collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage)));
24
}
25
26
27
public record ErrorDto(String error, Object details) {
28
public ErrorDto(final String error, final String detail) {
29
this(error, List.of(detail));
30
}
31
}
32
}

Ahora si intentamos guardar una instancia de ClientDTO con un rut inválido, obtendremos un mensaje de error más amigable:

{
"error": "Validation error",
"details": {
"rut": "must be a well-formed RUT (e.g. 12345678-9)"
}
}

Conclusión

En este post vimos cómo crear una anotación custom para validar un campo de rut en Spring Boot. Esto nos permite tener un código más limpio y fácil de mantener, ya que la lógica de validación está encapsulada en una anotación y un validador.

El código fuente de este post está disponible en fneiraj/JavaSpringExamples 🔗