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 🔗