Validation Contexts

A context is a blueprint for how an object will be validated. A context is identified by its path in the VSD and is recognized by straints as such if it has a child of one of the following:

  1. a constrain, include, or nested directive
  2. any validation level specified in configuration

For example,

person:
  constrain:
    name: [ is.notNull ]
    email: [ is.notNull, email ]
basketball:
  player:
    include: [ person ]
    constrain:
      position: [ is.playerPosition ]
  team:
    nested:
      coach:
        include: [ person ]
      players:
        nested:
          ____:
            include: [ basketball.player ]
    constrain:
      name: [ is.notNull ]
      coach: [ is.notNull ]
      players: [ is.notNull ]
is:
  - { name: notNull, test: 'null', flip: true }
  - { name: playerPosition, test: itemIn, params: [ [ point, guard, forward, water ] ] }

in the above our named contexts are

  • person
  • basketball.player
  • basketball.team
  • basketball.team.nested.coach
  • basketball.team.nested.players
  • basketball.team.nested.players.nested.____

This is because the 'person' context has a constrain directive, 'basketball.player' has include and constrain directives, 'basketball.team.nested.players' has nested, and so on.

A context may define its validations through 3 primary directives:

  • constrain - defines the required property validations for an object
  • include - conditionally includes additional contexts or portions thereof
  • nested - defines sub-contexts to test the validity of child objects

The Constrain Directive

The constrain directive applies validation rules to properties by name for its parent context. Any object being validated against that context will be required to satisfy these rules.

The usage of this directive is as follows:

(context name):
  constrain:
    (property 1):
      - (constraint A)
      - (constraint B)
      ...
    (property 2):
      - (constraint C)
      ...
    ...

For example:

add_user:
  constrain:
    name:
      - { test: missing, flip: true }
    email:
      - { test: missing, flip: true }
      - { test: email }

Here we have defined some rules for objects validated against the 'add_user' context.

  • property 'name' must not be missing
  • property 'email' must not be missing
  • property 'email' must be a valid email

It is also possible to specify validations by constraint.

(context name):
  constrain:
    ~(constraint A):
      - (property 1)
      - (property 2)
      ...
    ~(constraint B):
      - (property 2)
      ...
    ...

Note the tildes (~) used here. This lets the straints parser know that you are specifying validation rules by constraint rather than by property.

For example:

add_user:
  constrain:
    ~exists:
      - name
      - email
    ~email:
      - email

This validates similarly to the previous example.

  • property 'name' must exist
  • property 'email' must exist
  • property 'email' must be a valid email

The Quadruple Underscore

You may use ____ (quadruple underscore) as a property name in constrain to apply constraints to all properties that exist on the current validation target.

all_good:
  constrain:
    ____:
      - exists

This syntax works best with arrays, but can also be used with objects.

The Nested Directive

Use the nested directive when an object contains child objects or arrays that also need to be validated.

Here's the syntax:

(context name):
  nested:
    (property 1):
      ...
    (property 2):
      ...
    ...

Property names specified under nested are contexts by default and, therefore, enjoy all the benefits of being a context.

For example:

contact:
  constrain:
    address: [ exists ]
  nested:
    address:
      constrain:
        street: [ exists, string ]
        city: [ exists, string ]
        state: [ exists, string ]
        zipCode: [ exists, number ]

In the above, the 'contact' context allows validating the 'address' property as an object. It is also required to exist as per the constrain directive. Note here that any 'address' value that is not an object will simply be skipped by the nested directive. If we wish to make sure that 'address' is an object, we must further constrain it.

contact:
  constrain:
    address: [ exists, object ]

Here's an example of how this could work with an array (assuming values are positional).

contact:
  constrain:
    address: [ exists, array ]
  nested:
    address:
      constrain:
        0: [ exists, string ]
        1: [ exists, string ]
        2: [ exists, string ]
        3: [ exists, number ]

The nested directive (and straints in general) essentially treats arrays as objects with numeric keys.

The Quadruple Underscore

While nested alone works well for single objects, what if you want to validate a list of objects?

Remember our basketball VSD above? Here's the nested 'players' part of the 'basketball.team' context.

basketball:
  team:
    nested:
      players:
        nested:
          ____:
            include: basketball.player

We nest again inside of 'players' to get at the objects in the list. Then you can use the ____ to apply the context to every element of the array.

So, for every player on the team,

  • 'name' cannot be null
  • 'email' cannot be null
  • 'email' must be a valid email address
  • 'position' must be one of point, guard, forward, or water

The quad-underscore will also work on an object of objects as well. Just remember that ____ will apply its contextual validation rules to EVERY object found as a property value of the parent object.

The Include Directive

Perhaps we wish to reuse object validations and combine them to create more comprehensive validation. Here's a short (contrived) example. Imagine an e-commerce site that needs to capture details about the user making an order.

guest:
  constrain:
    ~exists: [ name, address, phone ]
    ~string: [ name, address, email ]
    phone: [ number ]
    email: [ email ]
login:
  constrain:
    ~exists: [ email, password ]
create_account:
  include: [ guest, login ]
  constrain:
    password: [ string, alphanumeric ]
    passwordConfirm:
      - exists
      - { test: equal, params: t.password }
    emailConfirm:
      - exists
      - { test: equal, params: t.email }

During checkout, if the user did not wish to create an account the incoming data object might be validated as 'guest'. Alternatively, validating with 'create_account' would be used if an account was desired.

The 'create_account' context includes the 'guest' and 'login' contexts. This means that all of the configuration for those two contexts is also applied when validating against 'create_account'.

Let's break down this example by context and property to see what's getting validated.

  • 'guest'
    • 'name' and 'address' must exist and be a string values.
    • 'phone' must exist and be a number.
    • 'email' must be string and a valid email address if it exists.
  • 'login'
    • 'email' and 'password' must exist.
  • 'create_account'
    • 'name' and 'address' must exist and be a string values.
    • 'phone' must exist and be a number.
    • 'email' must exist, be a string and a valid email address.
    • 'password' must exist, be a string and be alphanumeric in value.
    • 'passwordConfirm' must exist and be equal to 'password'.
    • 'emailConfirm' must exist and be equal to 'email'.

As you can see, validations are merged under constrain so that a given property is validated against an aggregation of constraints across all included contexts. Duplicate constraints are discarded, however, so a given constraint in a set of merged contexts will run only once against a given property.

Partial Inclusion

We can also include only specific directives from a context if we wish. For instance, we could rewrite the include for 'create_account' to include only the constrain directives from the contexts.

create_account:
  include: [ guest#constrain, login#constrain ]

Simply prefix the directive (or validation level) name with a hashmark (#).

Conditional Inclusion

Use a condition object to define which contexts will be included.

Imagine a situation where you need to select players for a basketball team.

potentialPlayer:
  include:
    - { if: greatShooter and greatPasser, then: starter, else: benchwarmer }

Validating against 'potentialPlayer' would first pre-validate the object as 'greatShooter' and 'greatPasser'. If these validate successfully, then the 'starter' context would be included with 'potentialPlayer', otherwise, the 'benchwarmer' context would be included.

Here are the parameters you can use to configure a condition object.

  • name (string)
    If you wish to reference the condition elsewhere in the VSD this makes it much easier.

  • if (string)
    A rule or rule expression that, if true, includes the then contexts. Otherwise, the else contexts will be included.

  • then (array|string)
    The context(s) that will be included when the if condition is true. If there is no if then inclusion is automatic.

  • else (array|string)
    The context(s) that will be included when the if condition is false. If there is no if then this is ignored.

Remember that while the if parameter accepts a rule, then and else can only accept an array or comma-delimited string of context names. Learn more about rules and rule expressions here.

NOTE
Validations that occur under an if condition are separate from the current validation session. They are not included in the results object nor do their executions generate callbacks.

Additional Validation Levels

In addition to the three directives, contexts can also be defined to have additional validation 'levels'. These levels must first be defined in the configuration to allow straints to recognize them.

var instance = straints({ levels: [ ... ] });

Then you can use those level names as children of the context. Their operation is identical to that of the constrain directive and their results are stored separately in the results object.

Note that you cannot use include, nested, or constrain as validation level names as they are reserved as context directives.