Validation

The validation component is a set utility functions that can be used to ensure any value respects a set of constraints. You can access it through the @banquette/validation package.

Basic usage

The validation component has been thought from the beginning to be as light and free of dependencies as possible, while still being very powerful. As such, there is no validation service, nor any centralized entity of any sort.

You can validate a value as easily as you would call a utility function:

import { Pattern } from "@banquette/validation";

Pattern(/^[A-Z]/).validate('Test').valid // true

Let's decompose the line:

  • First we call the function Pattern:
    Pattern(/^[A-Z]/)
    
    This function is validator factory. Its role is to create a validator with the correct configuration. Here we simply give it a pattern to match.
  • We then call validate on the resulting validator, with the value we want to validate as input:
    validate('Test')
    
  • The validate function returns a ValidationResult which contains a valid attribute that will be true if no error has been found, or false otherwise.

TIP

The validation can do much more. This first example is meant to show you that, even with all the complexity we will discuss next, a validator can always be used as a simple standalone function.


Validate an object

In the real world you would rarely want to validate a single value like this, so let's consider a basic User object:

const user = {
    firstName: 'John',
    lastName: 'Doe',
    email: 'john@doe.com'
};

Because we will use many validators, instead of importing them one by one, we will use a special object that contain all of them, called: V.

import { V } from "@banquette/validation";

const userValidator = V.Container({
    firstName: V.NotEmpty(),
    lastName: V.NotEmpty(),
    email: V.And(V.NotEmpty(), V.Email())
});

The first thing to note is that, no matter how complex your validation rules are, it all comes down to a single validator at the top of the tree.

Once this validator has been created, you can use it to validate as many values as you want:

// Validate the user we created above.
userValidator.validate(user); // Returns a ValidationResult object

// Validate another object.
userValidator.validate({
    firstName: 'Another',
    lastName: 'User',
    email: 'With invalid email'
}); // Returns a ValidationResult object

Let's briefly talk about the validators we've used:

  • Container: this is a special kind of validator. It doesn't validate anything itself, but will call other validators instead.
  • NotEmpty: check that the value is not empty.
  • And: execute each validator given to it until one of them fails

Do get a full description of each validator please check out the Built-in validators section that covers all of them in detail.

Validation masks

Imagine you have a big validation tree, composed of multiple objects and many rules, but you only want to validate one particular part of this object.

That's where you need a validation mask.

A mask is simply a string that supports basic glob pattern notation. You can give one or multiple masks to validate() with the mask option of any validator.

Basics

For all the examples below, let's use the following validator:

const validator = V.Container({
    name: V.NotEmpty(),
    email: V.And(V.NotEmpty(), V.Email(), V.Ajax('//email-not-used')),
    tags: V.Compose(
        V.Max(5),
        V.Foreach(
            V.Container({
                name: V.NotEmpty(),
                color: V.And(V.NotEmpty(), V.Pattern(/^#[0-9A-F]{6}$/))
            })
        )
    )
});

Here we add a single mask "/name" that will only match the root "name" attribute.

validator.validate(obj, {mask: '/name'});
// Will match: /name

To target both name AND email, we could set two masks:

validator.validate(obj, {mask: ['/name', '/email']});
// Will match: /name, /email

Or we could use a glob pattern to match both in a single mask:

validator.validate(obj, {mask: '/{name,email}'});
// Will match: /name, /email

We could make use of "*" to validate all properties of any tag:

validator.validate(obj, {mask: '/tags/*/*'});
// Will match: /tags/{N}/name, /tags/{N}/color

TIP

Please note that only the attributes of each tags are validated here. Not the collection itself (/tags) nor the array element (/tags/{N}), because of the double star (/*/*).


You can include all children To validate any child of /tags, no matter how deep, you can do:

validator.validate(obj, {mask: '/tags/**/*'});

// Will match: /tags/{N}, /tags/{N}/name, /tags/{N}/color

TIP

Note that the Max validator set on /tags is still not validated here because the /* limits the matching to children.


To validate everything related to tags, including /tags you must end with a globstar:

validator.validate(obj, {mask: '/tags/**'});

// Will match: /tags, /tags/{N}, /tags/{N}/name, /tags/{N}/color

You can match everything by not specifying any mask or by doing:

validator.validate(obj, {mask: '**'});
// Will match all validators.

Will match every validator except the root one (/), because of the /* that require the validator to have a parent:

validator.validate(obj, {mask: '/**/*'});
// Will match all validators except "/".

Modifiers

You can target synchronous or asynchronous validators by adding :sync or :async at the end of any mask. For example, to only validate synchronous validators:

validator.validate(obj, {mask: '/email:sync'});
// Will only process the NotEmpty() and Email() validators on the `/email` attribute.

And to only validate asynchronous validators:

validator.validate(obj, {mask: '/email:async'});
// Will only process the Ajax() validator on the `/email` attribute.

You can combine the modifiers with the glob patterns we've seen above:

validator.validate(obj, {mask: '/**/name:sync'});
// Will validate any validator with a path ending with "/name" that is synchronous.

Validation groups

Another way tou can filter what validators will apply is to use groups. This works in two phases:

1) Assign a group to the validator:

const validator = V.Container({
    subject: V.NotEmpty({groups: 'full'}),
    content: V.NotEmpty({groups: 'full'}),
    draftName: V.NotEmpty({groups: 'draft'}),
    mailbox: V.NotEmpty({groups: ['full', 'draft']}),
    tags: V.Max(5)
});

Here will validate:

  • subject, content and mailbox if the group is full,
  • draftName and mailbox if the group is draft.
  • tags only if no group is given to validate().

2) Use the group when validating:

// Only `draftName` and `mailbox` will be validated.
validator.validate(myObj, {group: 'draft'});

You can give an array of groups to validate too:

// Every validator expect `tags` will be validated.
validator.validate(myObj, {group: ['full', 'draft']});

If no group is present, only validators with no groups will validate:

// Only `tags` will be validated.
validator.validate(myObj);

Validation result

No matter what validator you execute, they all return a ValidationResult object. This object contains the following properties:

Attribute Description
path The absolute path to the value the result is for.
violations An array of ViolationInterface, each of them being an error that have been found.
valid A boolean holding if the validation succeeded.
invalid Inverse of valid.
error true if the validation failed to execute (e.g. an exception has been thrown)
waiting true if the validation is still running (async validator).
status Centralize the 4 flags seen above in a single value (enum).
promise A promise that will resolve when the validation is done (if async).
children Child valiation contexts.
parent Parent validation context.

The tree

So the ValidationResult is in fact a tree where each result can have any number of children.

There is a ValidationResult object created for each validator that is executed (with the exception of Virtual containers).

Each of these objects is identified by a unique path that follows the structure of the validation tree. Here is an example where the path of each validator is in comment:

V.Container({                                           // path: /
    name: V.NotEmpty(),                                 // path: /name
    email: V.And(                                       // path: /email
        V.NotEmpty(),                                   // path: /email (no change because the parent is a virtual container)
        V.Email()                                       // path: /email (same as above)
    ),
    tags: V.And(                                        // path: /tags
            V.Max(3),                                   // path: /tags (no change because the parent is a virtual container)
            V.Foreach(                                  // path: /tags (same as above)
                V.Container({                           // path: /tags/{N} (where N is the index in the array)
                    name: V.NotEmpty(),                 // path: /tags/{N}/name
                    color: V.Pattern(/^#[0-9]{3,6}$/)   // path: /tags/{N}/color
                })
            )
        )
});

Violations

Each ValidationResult can contain any number of violations.

A violation is a validation error. They implement the ViolationInterface which defines 3 attributes:

  • path: the path to the value that failed the validation (e.g. /category/name),
  • type: the type of error (usually the kebab-cased name of the validator that failed),
  • message: an optional error message describing the error.

Violations are added to the ValidationResult that belongs to the validator that added the violation, so they can be several levels deep.

For this reason, there are two methods at your disposal to get the violations you need:

  • getViolationsArray: Flattened the violations of all results matching the mask(s) into a single array. If no mask is given, all violations found in the tree will be returned.
getViolationsArray(mask?: string|string[]): ViolationInterface[]
  • getViolationsMap: Create an object containing the violations of all results matching the mask(s) indexed by violation path. If no mask is given, all violations found in the tree will be returned.
getViolationsMap(mask?: string|string[]): Record<string, ViolationInterface[]>

Both of the methods we've seen above take a mask parameter that will filter the violations to be returned. A mask is a simple string supporting basic glob pattern notation. For example, using our example above, we could do:

// Would get violations of the "/name" ValidationResult
result.getViolationsArray('/name')

// Two notations giving the same result.
result.getViolationsArray(['/name', '/email']); // Here we give two simple patterns (no glob)
result.getViolationsArray('/{name,email}');     // Here a glob pattern matching /name OR /email

// Here we only get errors related to tags names, no matter which tag
result.getViolationsArray('/tags/*/name');

// Here all errors related to any tags itself (so Max(3) is excluded here).
result.getViolationsArray('/tags/*/*');

// Match everything related to tags (Max(3) included). "**" matches {0,N} levels.
result.getViolationsArray('/tags/**');
result.getViolationsArray('/tags/**/**/**'); // Same as above.

// Will get NO ERROR because it only matches "/tags/{N}", not their children. And there is no validator on the array items themselves.
result.getViolationsArray('/tags/*');

// Here we get all violations (same as giving no mask at all).
result.getViolationsArray('/**'); // */
result.getViolationsArray('**'); // Same as above.

// Will get all errors except the root level because of the "/*".
result.getViolationsArray('/**/*');

// Will match every error with a path that ends with "/name".
result.getViolationsArray('/**/name');

TIP

We could have used getViolationsMap the exact same way, the mask works the same.

Sync vs Async

As seen above, calling validate() on a validator immediately returns a ValidationResult object. You may wonder what happens if the validator does some asynchronous task?

In such a case the promise attribute of the validator will be set by the validator:

import { Ajax } from '@banquette/validation';

Ajax('//example.com/is-email-available')          // Build the validator
    .validate('test@domain.tld')                  // Validate a value
    .promise.then((result: ValidationResult) => { // Wait for it to process
        if (result.valid) {                       // Do whatever with the result
            // ...
        }
    });

TIP

If you're confused on how the Ajax validator works, you can check out its documentation here. Here we will only concentrate on the async behavior.

As you can see, even if the validator is asynchronous we still immediately get a ValidationResult as result. The only differences with an synchronous validator are:

  • the promise attribute is set
  • the status is set to ValidationResultStatus.Waiting
  • both valid and invalid flags are false

TIP

Primitive validators will either be sync or async, not both. So you can rely on the fact that validating a synchronous validator will never set a promise.

Container validators can be sync or async, depending on their children.

For example:

const userValidator = Container({
    name: NotEmpty(),
    email: Email()
});

will always be sync, because both NotEmpty and Email are primitive synchronous validators.

Whereas:

const userValidator = Container({
    name: NotEmpty(),
    email: And(Email(), EmailNotUsed() /* Doing an ajax call */)
});

will be async if the Email() validator pass, because EmailNotUsed is asynchronous.

TIP

Even in the example above the validation can be synchronous if you set a Validation mask that excludes the EmailNotUsed() validator. For example :sync.

TLDR; a container will become asynchronous as soon as it encounter an async validator. Otherwise, it will remain sync.


Why?

Most of the time, the validation is synchronous. So the support of asynchronous validators must create as little overhead as possible.

Doing it like this has several advantages:

  • If you know all your validators are synchronous, you can use the validation as if asynchronous validation was not even supported. Its support have zero impact on what you write,
  • There is only 1 way of creating a validator and using it. We don't have to think about asynchronousity when building the validation tree,
  • You always immediately get a result object that you can store and make accessible to your view right away, even with async validators.

If you're in a scenario where you don't know if all your validators will be sync or not, you have to go async all the way. And there is the onReady() method for that:

unknownValidator        // A validator you don't know if sync or async
    .validate(value)    // Validate will always return a ValidationResult
    .onReady()          // Method part of ValidationResult that always return a promise
    .then((result: ValidationResult) => { // Called when the result is ready.
        // ...
    });

TIP

You could of course just check if the promise attribute is set, onReady is syntactic sugar.


Nested validators

When an async validator is executed, it will automatically make all its parents async too. This means you can call onReady (or then on the promise) at any level of the validation result tree, it will wait for all the children to be finished before resolving.

99% of the time you will only care about the top level validator though, so the fact that a validator deep in the tree takes some time to process is totally transparent. Just call onReady on the result of validate and you're golden.

Types of validators

There are 3 types of validators:

Primitive validators

The basic type of validator. They take a value and check something about it. If the condition doesn't validate, it adds a violation to the validation context.

Most validators are primitive, it includes: NotEmpty, Email, Phone, Min, Max, Equal, Ajax, Callback etc.

TIP

Primitive validators are the only type of validator that you should create. The other types of validator should already have every possible validator you may need.


Container

A container validator doesn't do any validation itself, it simply calls other validators. The important thing that differentiates it from a virtual container is that it creates a child validation context for each validator it calls.

This means that when you validate this:

const validator = Container({
    category: Container({
        name: NotEmpty()
    })
});

If the NotEmpty() constraint fails, the violation will have the following path: /category/name, because a validation context has been created for each level:

  • the first Container is "/"
  • it creates a sub context called "category"
  • the second Container creates a sub context called "name".

NOTE

There are only two types of containers: Container and Foreach.


Virtual container

Virtual containers are like normal containers with the exception that they don't create sub validation contexts when they call a validator.

This means if you do:

const validator = Container({
    category: Container({
        name: And(      // Virtual container
            NotEmpty(), 
            Or(         // Virtual container
                Min(10), Equal('-')
            )
        )
    })
});

A violation on name will always have the path /category/name, even if it occurred several levels deep in a tree of And or Or validators. That's because no sub context is created, so the validation path doesn't change.

NOTE

There are 4 types of virtual containers: And, Or, Compose and If.


All built-in validators are described in detail in the next chapter.