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 email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *