1. Overview

Validation is a frequently occurring task in Java applications, and hence a lot of effort has been put into the development of validation libraries.

Vavr (formerly known as Javaslang) provides a full-fledged validation API. It allows us to validate data in a straightforward manner, by using an object-functional programming style. If you want to peek at what this library offers out of the box, feel free to check this article.

In this tutorial, we take an in-depth look at the library’s validation API and learn how to use its most relevant methods.

2. The Validation Interface

Vavr’s validation interface is based on a functional programming concept known as an applicative functor. It executes a sequence of functions while accumulating the results, even if some or all of these functions fail during the execution chain.

The library’s applicative functor is built upon the implementers of its Validation interface. This interface provides methods for accumulating validation errors and validated data, therefore allowing to process both of them as a batch.

3. Validating User Input

Validating user input (e.g., data collected from a web layer) is smooth using the validation API, as it boils down to creating a custom validation class that validates the data while accumulating resulting errors if any.

Let’s validate a user’s name and email, which have been submitted via a login form. First, we need to include Vavr’s Maven artifact into the pom.xml file:

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.9.0</version>
</dependency>

Next, let’s create a domain class that models user objects:

public class User {
    private String name;
    private String email;
    
    // standard constructors, setters and getters, toString
}

Finally, let’s define our custom validator:

public class UserValidator {
    private static final String NAME_PATTERN = ...
    private static final String NAME_ERROR = ...
    private static final String EMAIL_PATTERN = ...
    private static final String EMAIL_ERROR = ...
    
    public Validation<Seq<String>, User> validateUser(
      String name, String email) {
        return Validation
          .combine(
            validateField(name, NAME_PATTERN, NAME_ERROR),
            validateField(email, EMAIL_PATTERN, EMAIL_ERROR))
          .ap(User::new);
    }
    
    private Validation<String, String> validateField
      (String field, String pattern, String error) {
 
        return CharSeq.of(field)
          .replaceAll(pattern, "")
          .transform(seq -> seq.isEmpty() 
            ? Validation.valid(field) 
            : Validation.invalid(error));        
    }
}

The UserValidator class validates the supplied name and email individually with the validateField() method. In this case, this method performs a typical regular expression based pattern matching.

The essence in this example is the use of the valid() , invalid() and combine() methods.

4. The valid(), invalid() and combine() Methods

If the supplied name and email match the given regular expressions, the validateField() method calls valid() . This method returns an instance of Validation.Valid. Conversely, if the values are invalid, the counter-part invalid() method returns an instance of Validation.Invalid.

This simple mechanism, based on creating different Validation instances depending on the validation results should give us at least a basic idea on how to process the results (more on this in section 5).

The most relevant facet of the validation process is the combine() method. Internally this method uses the Validation.Builder class, which allows to combine up to 8 different Validation instances that can be computed with different methods:

static <E, T1, T2> Builder<E, T1, T2> combine(
  Validation<E, T1> validation1, Validation<E, T2> validation2) {
    Objects.requireNonNull(validation1, "validation1 is null");
    Objects.requireNonNull(validation2, "validation2 is null");
    return new Builder<>(validation1, validation2);
}

The simplest Validation.Builder class takes two validation instances:

final class Builder<E, T1, T2> {

    private Validation<E, T1> v1;
    private Validation<E, T2> v2;

    // standard constructors

    public <R> Validation<Seq<E>, R> ap(Function2<T1, T2, R> f) {
        return v2.ap(v1.ap(Validation.valid(f.curried())));
    }

    public <T3> Builder3<E, T1, T2, T3> combine(
      Validation<E, T3> v3) {
        return new Builder3<>(v1, v2, v3);
    }
}

Validation.Builder, along with the ap(Function) method, returns one single result with the validation results. If all results are valid, the ap(Function) method maps the results onto a single value. This value is stored in a Valid instance by using the function specified in its signature.

In our example, if the supplied name and email are valid, a new User object is created. Of course, it is possible to do something entirely different with a valid result, i.e to stash it into a database, send it by email and so forth.

5. Processing Validation Results

It’s pretty easy to implement different mechanisms for processing validation results. But how do we validate data in the first place? To this extent, we use the UserValidator class:

UserValidator userValidator = new UserValidator(); 
Validation<Seq<String>, User> validation = userValidator
  .validateUser("John", "[email protected]");

Once an instance of Validation is obtained, we can leverage the flexibility of the validation API and process results in several ways.

Let’s elaborate on the most commonly encountered approaches.

5.1. The Valid and Invalid Instances

This approach is the simplest one by far. It consists of checking validation results with the Valid and Invalid instances:

@Test
public void 
  givenInvalidUserParams_whenValidated_thenInvalidInstance() {
    assertThat(
      userValidator.validateUser(" ", "no-email"), 
      instanceOf(Invalid.class));
}
    
@Test
public void 
  givenValidUserParams_whenValidated_thenValidInstance() {
    assertThat(
      userValidator.validateUser("John", "[email protected]"), 
      instanceOf(Valid.class));
}

Rather than checking the validity of results with the Valid and Invalid instances, we should just go one step further and use the isValid() and isInvalid() methods.

5.2. The isValid() and isInvalid() APIs

Using the tandem isValid() / isInvalid() is analogous to the previous approach, with the difference that these methods return true or false, depending on the validation results:

@Test
public void 
  givenInvalidUserParams_whenValidated_thenIsInvalidIsTrue() {
    assertTrue(userValidator
      .validateUser("John", "no-email")
      .isInvalid());
}

@Test
public void 
  givenValidUserParams_whenValidated_thenIsValidMethodIsTrue() {
    assertTrue(userValidator
      .validateUser("John", "[email protected]")
      .isValid());
}

The Invalid instance contains all the validation errors. They can be fetched with the getError() method:

@Test
public void 
  givenInValidUserParams_withGetErrorMethod_thenGetErrorMessages() {
    assertEquals(
      "Name contains invalid characters, Email must be a well-formed email address", 
      userValidator.validateUser("John", "no-email")
        .getError()
        .intersperse(", ")
        .fold("", String::concat));
 }

Conversely, if the results are valid, a User instance can be grabbed with the get() method:

@Test
public void 
  givenValidUserParams_withGetMethod_thenGetUserInstance() {
    assertThat(userValidator.validateUser("John", "[email protected]")
      .get(), instanceOf(User.class));
 }

This approach works as expected, but the code still looks pretty verbose and lengthy. We can compact it further using the toEither() method.

5.3. The toEither() API

The toEither() method constructs Left and Right instances of the Either interface. This complementary interface has several convenience methods that can be used for shortening the processing of validation results.

If the results are valid, the result is stored in the Right instance. In our example, this would amount to a valid User object. Conversely, if the results are invalid, the errors are stored in the Left instance:

@Test
public void 
  givenValidUserParams_withtoEitherMethod_thenRightInstance() {
    assertThat(userValidator.validateUser("John", "[email protected]")
      .toEither(), instanceOf(Right.class));
}

The code now looks much more concise and streamlined. But we’re not done yet. The Validation interface provides the fold() method, which applies a custom function that applies to valid results and another one to invalid ones.

5.4. The fold() API

Let’s see how to use the fold() method for processing validation results:

@Test
public void 
  givenValidUserParams_withFoldMethod_thenEqualstoParamsLength() {
    assertEquals(2, (int) userValidator.validateUser(" ", " ")
      .fold(Seq::length, User::hashCode));
}

The use of fold() reduces the processing of validation results to just a one-liner.

It’s worth stressing that the functions’ return types passed as arguments to the method must be the same. Moreover, the functions must be supported by the type parameters defined in the validation class, i.e., Seq and User.

6. Conclusion

In this article, we explored in depth Vavr’s validation API and learned how to use some of its most relevant methods. For a full list, check the official docs API.

Vavr’s validation control provides a very appealing alternative to more traditional implementations of Java Beans Validation, such as Hibernate Validator.

As usual, all the examples shown in the article are available over on GitHub.


« 上一篇: Java递归删除文件夹
» 下一篇: "Sneaky Throws" 用法