Edit Page

Validations

Sails bundles support for automatic validations of your models' attributes. Any time a record is updated, or a new record is created, the data for each attribute will be checked against all of your predefined validation rules. This provides a convenient failsafe to ensure that invalid entries don't make their way into your app's database(s).

Except for unique (which is implemented as a database-level constraint; see "Unique"), all validations below are implemented in JavaScript and run in the same Node.js server process as Sails. Also keep in mind that, no matter what validations are used, an attribute must always specify one of the built-in data types (string, number, json, etc).

// User
module.exports = {
  attributes: {
    emailAddress: {
      type: 'string',
      unique: true,
      required: true
    }
  }
};

Built-in data types

#

In Sails/Waterline, model attributes always have some kind of data type guarantee. This is above and beyond any physical-layer constraints which might exist in your underlying database—it's more about providing a way for developers to maintain reasonable assumptions about the data that goes in or comes out of a particular model.

This data type guarantee is used for logical validation and coercion of results and criteria. Here is a list of the data types supported by Sails and Waterline:

Data Type Usage Description
String type: 'string' Any string.
Number type: 'number' Any number.
Boolean type: 'boolean' true or false.
JSON type: 'json' Any JSON-serializable value, including numbers, booleans, strings, arrays, dictionaries (plain JavaScript objects), and null.
Ref type: 'ref' Any JavaScript value except undefined. (Should only be used when taking advantage of adapter-specific behavior.)

Sails' ORM (Waterline) and its adapters perform loose validation to ensure that the values provided in criteria dictionaries and as values to .create() or .update() match the expected data type.

NOTE: In adapters that don't support the JSON type natively, the adapter must support it in other ways. For example, in MySQL the data being written to a JSON attribute gets JSON.stringify() called on it and then is stored in a column with a type set to text. Each time the record is returned, the data has JSON.parse() called on it. This is something to be aware of when considering performance and compatibility with other applications or existing data in the database. The official PostgreSQL and mongoDB adapters can read and write JSON data natively.

Null and empty string
#

The string, number and boolean data types do not accept null as a value when creating or updating records. In order to allow a null value to be set, toggle the allowNull flag on the attribute. The allowNull flag is only valid on the above data types; it is not valid on attributes with types json or ref, any associations, or any primary key attributes.

Since empty string ("") is a string, it is normally supported by type: 'string' attributes; but there are a couple of exceptions: primary keys (because primary keys never support empty string) and any attribute which has required: true.

Required
#

If an attribute is required: true, then a value must always be specified for it when calling .create(). This also prevents a value from being set to null or empty string ("") when created or updated.

Validation rules

#

None of the following validation rules impose any additional restrictions against null. That is, if null would be allowed normally, then enabling the isEmail validation rule will not cause null to be rejected as invalid.

Similarly, most of the following validation rules don't impose any additional restrictions against empty string (""). There are a few exceptions (isNotEmptyString and non-string-related rules like isBoolean, isNumber, max, and min), but otherwise, for any attribute where empty string ("") would normally be allowed, adding a validation rule will not cause it to be rejected.

In the table below, the "Compatible Attribute Type(s)" column shows what data type(s) (i.e. for the attribute definition's type property) are appropriate for each validation rule. In many cases, a validation rule can be used with more than one type. Note that the table below takes a shortcut: if compatible with String, Number, or Boolean, then the validation rule is also compatible with JSON and Ref, even if it doesn't explicitly say so.

Name of Rule What It Checks For Notes On Usage Compatible Attribute Type(s)
custom A value such that when it is provided as the first argument to the custom function, the function returns true. Example Any
isAfter A value that, when parsed as a date, refers to a moment after the configured JavaScript Date instance. isAfter: new Date('Sat Nov 05 1605 00:00:00 GMT-0000') String, Number
isBefore A value that, when parsed as a date, refers to a moment before the configured JavaScript Date instance. isBefore: new Date('Sat Nov 05 1605 00:00:00 GMT-0000') String, Number
isBoolean A value that is true or false isBoolean: true JSON, Ref
isCreditCard A value that is a credit card number. Do not store credit card numbers in your database unless your app is PCI compliant! If you want to allow users to store credit card information, a safe alternative is to use a payment API like Stripe. String
isEmail A value that looks like an email address. isEmail: true String
isHexColor A string that is a hexadecimal color. isHexColor: true String
isIn A value that is in the specified array of allowed strings. isIn: ['paid', 'delinquent'] String
isInteger A number that is an integer (a whole number) isInteger: true Number
isIP A value that is a valid IP address (v4 or v6) isIP: true String
isNotEmptyString A value that is not an empty string isNotEmptyString: true JSON, Ref
isNotIn A value that is not in the configured array. isNotIn: ['profanity1', 'profanity2'] String
isNumber A value that is a Javascript number isNumber: true JSON, Ref
isString A value that is a string (i.e. typeof(value) === 'string') isString: true JSON, Ref
isURL A value that looks like a URL. isURL: true String
isUUID A value that looks like a UUID (v3, v4 or v5) isUUID: true String
max A number that is less than or equal to the configured number. max: 10000 Number
min A number that is greater than or equal to the configured number. min: 0 Number
maxLength A string that has no more than the configured number of characters. maxLength: 144 String
minLength A string that has at least the configured number of characters. minLength: 8 String
regex A string that matches the configured regular expression. regex: /^[a-z0-9]$/i String
Example: optional email address
#

Imagine that you have an attribute defined as follows:

workEmail: {
  type: 'string',
  isEmail: true,
}

When you call .create() _or_ .update(), this value can be set to any valid email address (like "santa@clause.com") OR to an empty string (""). You would not be able to set it to null, though, because that would violate the type safety restriction imposed by type: 'string'.

To make this attribute accept null (e.g. if you are working with a pre-existing database), change it to type: 'json'. You'd normally also want to add isString: true, but since we already enforce isEmail: true in this example, there's no need to do so.

A more advanced feature to keep in mind is that, depending on your database, you can choose to take advantage of columnType to inform Sails / Waterline which column type to define during auto-migrations (if relevant).

Example: required star rating
#

If we want to indicate that an attribute supports certain numbers, like a star rating, we might do something like this:

starRating: {
  type: 'number',
  min: 1,
  max: 5,
  required: true,
}
Example: optional star rating
#

If we want to make our star rating optional, it's easiest to just remove the required: true flag. If omitted, the starRating will default to zero.

Example: optional star rating (with null)
#

But what if the star rating can't always be a number? Imagine we need to integrate with a legacy database in which star ratings could be either a number or the special null literal. In this scenario, we would like to define the starRating attribute to support both certain numbers and null.

To accomplish this, just use allowNull:

starRating: {
  type: 'number',
  allowNull: true,
  min: 1,
  max: 5,
}

Sails and Waterline attributes support allowNull for convenience, but another viable solution is to change starRating from type: 'number' to type: 'json'. Remember, though, that the json type allows other data, like booleans, arrays, etc. If we want to explicitly protect against those data types being supported by starRating, we could add the isNumber: true validation rule:

starRating: {
  type: 'json',
  isNumber: true,
  min: 1,
  max: 5,
}

Unique

#

unique is different from all of the validation rules listed above. In fact, it isn't really a validation rule at all: it is a database-level constraint. More on that in a second.

If an attribute declares itself unique: true, then Sails ensures that no two records will be allowed with the same value. The canonical example is an emailAddress attribute on a User model:

// api/models/User.js
module.exports = {

  attributes: {
    emailAddress: {
      type: 'string',
      unique: true,
      required: true
    }
  }

};
Why is unique different from other validations?
#

Imagine you have 1,000,000 user records in your database. If unique was implemented like other validations, every time a new user signed up for your app, Sails would need to search through one million existing records to ensure that no one else was already using the email address provided by the new user. That would be so slow that by the time we finished searching through all those records, someone else could have signed up!

Fortunately, this type of uniqueness check is perhaps the most universal feature of any database. To take advantage of that, Sails relies on the database adapter to implement support for unique—specifically, it adds a uniqueness constraint to the relevant field/column/attribute in the database itself during auto-migration. That is, while your app is set to migrate:'alter', Sails will automatically generate tables/collections in the underlying database with uniqueness constraints built right in. Once you switch to migrate:'safe', updating your database constraints is up to you.

What about indexes?
#

When you start using your production database, it is always a good idea to set up indexes to boost your database's performance. The exact process and best practices for setting up indexes varies between databases and is beyond the scope of this documentation. That said, if you've never done this before, don't worry: it's easier than you think.

Just like everything else related to your production schema, once you set your app to use migrate: 'safe', Sails leaves database indexes entirely up to you.

Note that this means you should be sure to update your indexes alongside your uniqueness constraints when performing manual migrations.

When to use validations

#

Validations can save you from writing many hundreds of lines of repetitive code, but keep in mind that model validations are run for every create or update in your application. Before using a validation rule in one of your attribute definitions, make sure you are okay with it being applied every time your application calls .create() or .update() to specify a new value for that attribute. If that is not the case, write code that validates the incoming values inline in your controller, or call a custom function in one of your services or a model class method.

Suppose that your Sails app allows users to sign up for an account by either (A) entering an email address and password and then confirming that email address or (B) signing up with LinkedIn. Your User model might have one attribute called manuallyEnteredEmail and another called linkedInEmail. While one of those email address attributes is required, which one that is depends on how a user signs up. In this case, your User model cannot use the required: true validation. In order to confirm that one of the two emails has been provided—and that the provided email is valid—you'll instead have to manually check these values before the relevant .create() and .update() calls in your code:

if ( !_.isString( req.param('email') ) ) {
  return res.badRequest();
}

Taking this one step further, let's say your application accepts payments. During the sign-up flow, if the user signs up with a paid plan, they must provide an email address for billing purposes (billingEmail), while if the user signs up with a free account, they skip that step. On the account settings page, users on the paid plan see a "Billing Email" form field where they can customize their billing email. Users with the free plan, on the other hand, see a call to action which links to the "Upgrade Plan" page.

While these requirements seem specific, there are still unanswered questions:

Depending on the answers to questions like these, we might end up keeping the required validation on billingEmail, adding new attributes (like hasBillingEmailBeenChangedManually), or even rethinking whether we use a unique constraint.

Best practices

#

Finally, here are a few tips:

As much as possible, it is best to obtain or flesh out your own wireframes of your app's user interface before you spend any serious amount of time implementing any backend code. Of course, this isn't always possible, and that's what the blueprint API is for. Applications built with a UI-centric, or "front-end first", philosophy are easier to maintain, tend to have fewer bugs, and—since mindfulness of the user experience is at their core—often have more elegant APIs.

Custom validation rules

#

You can define your own custom validation rules by specifying a custom function in your attributes.

// api/models/User.js
module.exports = {

  // Values passed for creates or updates of the User model must obey the following rules:
  attributes: {

    firstName: {
      // Note that a base type (in this case "string") still has to be defined, even though validation rules are in use.
      type: 'string',
      required: true,
      minLength: 5,
      maxLength: 15
    },

    location: {
      type: 'json',
      custom: function(value) {
        return _.isObject(value) &&
        _.isNumber(value.x) && _.isNumber(value.y) &&
        value.x !== Infinity && value.x !== -Infinity &&
        value.y !== Infinity && value.y !== -Infinity;
      }
    },

    password: {
      type: 'string',
      custom: function(value) {
        // • be a string
        // • be at least 6 characters long
        // • contain at least one number
        // • contain at least one letter
        return _.isString(value) && value.length >= 6 && value.match(/[a-z]/i) && value.match(/[0-9]/);
      }
    }

  }

}

Custom validation functions receive the incoming value to be validated as their first argument. They are expected to return true if valid and false otherwise.

Custom validation messages
#

Out of the box, Sails.js does not support custom validation messages. Instead, your code should look at (or "negotiate") validation errors thrown by .create() or .update() calls and take the appropriate action, whether that's sending a particular error code in your JSON response or rendering the appropriate message in an HTML error page.

Is something missing?

If you notice something we've missed or could be improved on, please follow this link and submit a pull request to the sails repo. Once we merge it, the changes will be reflected on the website the next time it is deployed.

Concepts