Java Bean Validation – niestandardowy walidator danych

Czasem zachodzi potrzeba sprawdzenia danych, z którymi domyślne walidatory sobie nie poradzą. Możemy wtedy stworzyć sobie własny walidator, który sprawdzi nam poprawność danych. Zobaczmy jak to można zrobić.

Post jest kontynuacją serii o Java Bean Validation. W poprzednich częściach pisałem, jak można programatycznie sprawdzać poprawność danych (Java Bean Validation – sprawdzanie poprawności przesłanych danych) oraz jak to działa w połączeniu ze springiem (Java Bean Validation + Spring Boot – sprawdzanie poprawności danych w Spring Boocie). Kod źródłowy do wszystkich wpisów znajduje się w GitHubie, pod adresem: https://github.com/mloza/spring-boot-bean-validation.

Dla potrzeb przykładu załóżmy, że chcemy sprawdzać poprawność kodu pocztowego. Możemy to zrobić prostym RegExem, ale używamy tego na tyle często, że chcemy mieć osobny walidator.

Krok I: Adnotacja

Zacznijmy od stworzenia adnotacji, którą będziemy używać do oznaczania pola, które powinno zostać sprawdzone przez nasz niestandardowy walidator danych. Może ona wyglądać następująco:

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = ZipCodeValidator.class). // #1
@Documented
public @interface ZipCode {
String message() default "Podany kod pocztowy jest nieprawidłowy"; // #2
Class<?>[] groups() default {}; // #3
Class<? extends Payload>[] payload() default {}; // #4
}
view raw ZipCode.java hosted with ❤ by GitHub

Naszą adnotację adnotujemy @Constraint (#1) i podajemy w validatedBy nazwę naszej klasy, która będzie dokonywała sprawdzenia (znajduje się poniżej). Następnie definiujemy, jaką wiadomość ma zwracać, gdy walidacja się nie powiedzie (#2). groups definiuje nam grupy walidacji, tego jeszcze nie omawiałem, możliwe, że pojawi się o tym oddzielny post, na razie możemy to pominąć. payload również nie omawiałem i na razie je pomińmy. (Jeśli chcecie o tym przeczytać więcej, dajcie znać w komentarzach)

Krok II: Walidator

Jeśli już mamy naszą adnotację to teraz możemy się zająć kodem do sprawdzania poprawności danych. Może on wyglądać następująco.

public class ZipCodeValidator implements ConstraintValidator<ZipCode, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
Pattern pattern = Pattern.compile("^[0-9]{2}-[0-9]{3}$");
return pattern.matcher(value).matches();
}
}

Nasz strażnik poprawności danych jest bardzo prosty. Klasa musi dziedziczyć po ConstraintValidator, który przyjmuje dwa parametry jako typy: pierwszy to typ adnotacji, drugi to typ danych, do których może być stosowany. W naszym przypadku podajemy adnotację ZipCode oraz typ String, czyli pola zawierające łańcuch znaków będą mogły być adnotowane.

Krok III: Użycie

Mamy już wszytko czego potrzebowaliśmy, aby przeprowadzić niestandardową walidację. Teraz możemy jej użyć. Zacznijmy od stworzenia klasy z polem zawierającym kod pocztowy.

public class RequestWithZipCode {
@ZipCode // #1
private String zipCode;
// geter, seter, konstruktory...
}

Bardzo prosta klasa zawierająca tylko jedno pole z naszą adnotacją @ZipCode.

Potrzebujemy ją gdzieś użyć. Stwórzymy więc kontroler, który przyjmie tę klasę jako body zapytania.

@RestController
public class CustomValidatorController {
@PostMapping("/zipCode")
@ResponseBody
public String zipCodeBody(
@RequestBody @Valid RequestWithZipCode body // #1
) {
return "OK! Everything is valid";
}
}

Jak widzicie, nic się nie zmieniło w porównaniu do dotychczasowych przykładów z poprzednich postów. Znów mamy adnotację @Valid, która mówi Springowi, że to pole musi zostać sprawdzone. Zobaczmy czy tak się stanie.

Krok IV: Test

Aby szybko sprawdzić, czy nasza adnotacja działa, możemy stworzyć sobie test. Stworze tylko dwa przypadki testowe, aby sprawdzić, czy coś się w ogóle stanie. Cały test poniżej. Zawiera on więcej kodu niż wszystko, co do tej pory widziałeś razem wzięte 😀

@WebMvcTest
public class CustomValidatorControllerTest {
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper objectMapper;
@Test
public void shouldReturnErrorWhenInputIsInvalid() throws Exception {
mvc.perform(post("/zipCode")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidBody()))
.andDo(mvcResult -> System.out.println(mvcResult.getResponse().getContentAsString()))
.andExpect(status().isBadRequest());
}
@Test
public void shouldReturnOkWhenInputIsValid() throws Exception {
mvc.perform(post("/zipCode")
.contentType(MediaType.APPLICATION_JSON)
.content(validBody()))
.andDo(mvcResult -> System.out.println(mvcResult.getResponse().getContentAsString()))
.andExpect(status().is2xxSuccessful());
}
private String invalidBody() throws JsonProcessingException {
return objectMapper.writeValueAsString(
new RequestWithZipCode("1-21"));
}
private String validBody() throws JsonProcessingException {
return objectMapper.writeValueAsString(
new RequestWithZipCode("31-521"));
}
}

Po odpaleniu testu wszytko powinno być zielone. Na konsolce powinny być wiadomości podobne do tych:

{"errors":[{"fieldName":"zipCode","message":"Podany kod pocztowy jest nieprawidłowy"}]}
OK! Everything is valid

Pierwsza linia jest zwracana przez test przesyłający niepoprawne dane. Druga gdy dane są poprawne. Wygląda więc, że nasz walidator działa!

Podsumowanie

Widziałeś już jak w bardzo prosty sposób można stworzyć sobie niestandardowy walidator danych. Jest to bardzo łatwe i może nam zaoszczędzić wiele czasu z kombinowaniem używając standardowych ograniczeń, lub poprawić czytelność jeśli zamienimy bardzo długi regex na coś takiego.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *