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.
12 collapsed lines
1package dev.fneira.customvalidation.validation;2
3import static java.lang.annotation.ElementType.FIELD;4import static java.lang.annotation.ElementType.PARAMETER;5
6import jakarta.validation.Constraint;7import jakarta.validation.Payload;8import java.lang.annotation.Documented;9import java.lang.annotation.Retention;10import java.lang.annotation.RetentionPolicy;11import java.lang.annotation.Target;12
13
14@Target({FIELD, PARAMETER})15@Retention(RetentionPolicy.RUNTIME)16@Documented17
18
19@Constraint(validatedBy = RutValidator.class)20public @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.
5 collapsed lines
1package dev.fneira.customvalidation.validation;2
3import jakarta.validation.ConstraintValidator;4import jakarta.validation.ConstraintValidatorContext;5
6
7public class RutValidator implements ConstraintValidator<Rut, String> {8
9 @Override10 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:
7 collapsed lines
1package dev.fneira.customvalidation.dto;2
3import dev.fneira.customvalidation.validation.Rut;4import jakarta.validation.constraints.Email;5import jakarta.validation.constraints.NotBlank;6import java.time.LocalDate;7
8public 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 setters18 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:
11 collapsed lines
1package dev.fneira.customvalidation.controller;2
3import dev.fneira.customvalidation.dto.ClientDTO;4import dev.fneira.customvalidation.service.ClientService;5import jakarta.validation.Valid;6import org.springframework.beans.factory.annotation.Autowired;7import org.springframework.web.bind.annotation.PostMapping;8import org.springframework.web.bind.annotation.RequestBody;9import org.springframework.web.bind.annotation.RequestMapping;10import org.springframework.web.bind.annotation.RestController;11
12@RestController13@RequestMapping("/client")14public class ClientController {15
16 @Autowired private ClientService clientService;17
18 @PostMapping19
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:
11 collapsed lines
1package dev.fneira.customvalidation.controller;2
3import java.util.List;4import java.util.stream.Collectors;5import org.springframework.http.HttpStatus;6import org.springframework.validation.FieldError;7import org.springframework.web.bind.MethodArgumentNotValidException;8import org.springframework.web.bind.annotation.ExceptionHandler;9import org.springframework.web.bind.annotation.ResponseStatus;10import org.springframework.web.bind.annotation.RestControllerAdvice;11
12@RestControllerAdvice13public 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 🔗