Skip to main content

Validated

The Validated type, like Either represents the existence of one of two types. An instance of Validated is an instance of Valid or Invalid.

Additionaly ValidatedNel<E, A> is an alias of type Validated<NonEmptyIList<E>, A> which describes either one or more errors (NonEmptyIList<A>>) or a successful value (A).

info

This introduces a new datastructure used in Ribs, NonEmptyIList, which is a list that contains at least one element. You can read more about it on the NonEmptyIList page, but for the purposes of Validated just understand it's a list with at least one element.

Motivation

Let's expand and improve on the example from Either where we wanted to define a function to create a new user from a set of inputs. Here's our domain model:

Our User Model
// Some type aliases for clarity
typedef Name = String;
typedef Alias = String;
typedef Age = int;

final class User {
final Name name;
final Alias alias;
final Age age;

const User(this.name, this.alias, this.age);
}

Either<String, User> userEither(Name name, Alias alias, Age age) =>
throw UnimplementedError();

Remember that our function built on Either was capable of returning a reason as to why the user couldn't be created (e.g. no name provided, too young, etc.).

But consider the case where there are multiple issues with the input. In our previous implementation, once an error is encountered, that error is returned and we can try again, only to run into yet another error. Frustrating to be sure!

In this case Validated can help us by accumuulating all validation errors in case of a failure or returning the validated user. Let's take a quick look at what this could look like:

Utilizing ValidatedNel
ValidatedNel<String, Name> validateName(Name name) =>
name.isEmpty ? 'No name provided!'.invalidNel() : name.validNel();

ValidatedNel<String, Alias> validateAlias(Alias alias) =>
alias.isEmpty ? 'No alias provided!'.invalidNel() : alias.validNel();

ValidatedNel<String, Age> validateAge(Age age) =>
age < 18 ? 'Too young!'.invalidNel() : age.validNel();

ValidatedNel<String, User> createUser(User user) => (
validateName(user.name),
validateAlias(user.alias),
validateAge(user.age),
).mapN(User.new);

With this definition, we can validate all the individual pieces of our user data and if everything looks good, get our user! If one or more of the pieces doesn't pass the check, we'll get information about everything that needs fixed! Here's how it works in practice:

Validated Output
// Valid(Instance of 'User')
final good = createUser(const User('John', 'Doe', 30));

// Invalid(NonEmptyIList(No name provided!))
final noName = createUser(const User('', 'Doe', 30));

// Invalid(NonEmptyIList(Too young!))
final tooYoung = createUser(const User('John', 'Doe', 7));

// Invalid(NonEmptyIList(No name provided!, Too young!))
final noAliasAndTooYoung = createUser(const User('John', '', 10));

// Invalid(NonEmptyIList(No name provided!, No alias provided!))
final noNameNoAlias = createUser(const User('', '', 75));

Once we run our user through the validation, we can decide what action(s) to take. If the validation failed, we may show an error. If we succeed, maybe store the user to a database.

Reating to ValidateNel
final succeeded = createUser(const User('John', 'Doe', 30));
final failed = createUser(const User('', 'Doe', 3));

void notifyUser(String message) => throw UnimplementedError();
void storeUser(User user) => throw UnimplementedError();

void handleCreateUser() {
succeeded.fold(
(errors) =>
notifyUser(errors.mkString(start: 'User creation failed: ', sep: ',')),
(user) => storeUser(user),
);
}