Security News
JSR Working Group Kicks Off with Ambitious Roadmap and Plans for Open Governance
At its inaugural meeting, the JSR Working Group outlined plans for an open governance model and a roadmap to enhance JavaScript package management.
The Joi package is a powerful schema description language and data validator for JavaScript. It allows developers to create blueprints or schemas for JavaScript objects to ensure validation of key information. It is commonly used for validating data, such as configuration objects, request data in web applications, and more.
Object Schema Validation
This feature allows you to define a schema for an object, specifying the expected type of each property, and then validate data against this schema. The code sample demonstrates creating a schema for user data and validating an object against it.
{"const Joi = require('joi');\nconst schema = Joi.object({\n username: Joi.string().alphanum().min(3).max(30).required(),\n password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')),\n repeat_password: Joi.ref('password'),\n access_token: [Joi.string(), Joi.number()],\n birth_year: Joi.number().integer().min(1900).max(2013),\n email: Joi.string().email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } })\n}).with('username', 'birth_year');\n\nconst dataToValidate = {\n username: 'abc',\n birth_year: 1994,\n email: 'test@example.com'\n};\n\nconst result = schema.validate(dataToValidate);\nconsole.log(result);"}
Type Casting
Joi can cast values to other types as part of the validation process. In this example, a string representing a number is cast to an actual number, and a string 'TRUE' is cast to a boolean true.
{"const Joi = require('joi');\nconst schema = Joi.object({\n age: Joi.number().integer(),\n active: Joi.string().trim().lowercase().valid('true', 'false').truthy('true').falsy('false').default(false).cast('boolean')\n});\n\nconst dataToValidate = {\n age: '30',\n active: 'TRUE'\n};\n\nconst result = schema.validate(dataToValidate, { convert: true });\nconsole.log(result.value); // { age: 30, active: true }"}
Custom Validation Messages
Joi allows you to specify custom validation messages for when data does not conform to the schema. This example shows how to set a custom message for a string that is too short.
{"const Joi = require('joi');\nconst schema = Joi.string().min(10).message('The string needs to be at least 10 characters long.');\n\nconst result = schema.validate('short');\nconsole.log(result.error); // Custom error message"}
Yup is a JavaScript schema builder for value parsing and validation. It is similar to Joi but is leaner and designed for browser usage as well as Node.js. It uses a similar API to Joi but is built with a different focus on client-side validation.
Ajv (Another JSON Schema Validator) is a fast JSON schema validator supporting JSON Schema draft-06/07/2019-09/2020-12 and JSON Type Definition (RFC 8927). It is different from Joi in that it uses a JSON-based schema definition rather than a fluent API.
Validator is a library of string validators and sanitizers. It is not a schema-based validation library like Joi but provides a set of string validation functions that can be used individually to validate specific types of input data.
Class-validator works with TypeScript and uses decorators to define validation rules within class definitions. It is different from Joi in that it integrates tightly with TypeScript and leverages its type system for validation.
Object schema validation
The joi validation system is used to validate JavaScript objects based on a rich descriptive schema. Schema validation is the process of ensuring that objects match pre-defined expectations.
For example, the following schema:
var Joi = require('joi');
var schema = {
username: Joi.types.String().alphanum().min(3).max(30).with('birthyear').required(),
password: Joi.types.String().regex(/[a-zA-Z0-9]{3,30}/).without('access_token'),
access_token: Joi.types.String(),
birthyear: Joi.types.Number().min(1850).max(2012),
email: Joi.types.String().email()
};
defines these constraints:
The above constraints point out some non-obvious features:
Below is an example of how to validate an object against the above schema:
Joi.validate(obj, schema, function (err) {
// err will be set if the object failed to validate against the schema
});
The Types object is pre-populated with a mutable list of JavaScript's valid data types. However, for convenience, the registry also includes subset helpers with common constraints already applied. For a list of helpers see the following sections...
Any custom, user-defined data type is derived from one of the base types (although it may also combine additional types for sub-elements). Thus, there are two valid ways of creating your own types.
The first method is to add the type directly to the Type Registry. This makes the new type explicitly available as a base Type.
var IntDef = _.extends({}, Number, function () {
// Constructor
return this.integer();
});
Types.set("Int", IntDef);
var Int = Types.Int;
The second, simpler, and more acceptable method is to alias the new Type within the config file.
var PositiveInt = Number().integer().min(0)
PositiveInt.max(999999);
Thus, subsequent calls to the new "type" will behave as fully registered types in much less code.
Note: The first method may eventually be deprecated. Then, the Type Registry becomes non-mutable which simplies the logic significantly.
Note: See "Reference A" before suggesting a pre-included Type for the Type Registry.
Constraints are functions that restrict the input value in some way.
By default, all without explicit constraints, Types are optional.
var schema = {
username: Joi.types.String().min(6).max(30).allow('admin').deny('Administrator'),
};
The above example demonstrates that even though the username has a minimum length of 6, extra constraints can be appended that allow 'admin' to be used as a username. Likewise, even though 'Administrator' would be allowed by the other constraints, it is explicitly denied by the 'deny' constraint.
All types inherit the following builtin constraints:
Specifies that the input may not be undefined (unspecified).
Specifies that the input may equal this value. This is type specific, so you cannot allow a number on a string type and vice-versa.
This function is idempotent.
Note: This function does not verify that value is the correct type.
Specifies that the input may NOT equal this value.
This function is idempotent.
Note: This function does not verify that value is the correct type.
Specifies an arbitrary number of valid values for this input.
If no inputs are supplied, it returns an Error.
If one or more of inputs given do not match the basic type, an Error is raised.
Specifies an arbitrary number of invalid values for this input.
If no inputs are supplied, it returns an Error.
If one or more of inputs given do not match the basic type, an Error is raised.
Specifies an arbitrary number of inputs that must also be supplied (a1..an) with this input.
Note: This may or may not have aliases in the final version (.join, .with, .and... etc)
Specifies an arbitrary number of inputs that cannot exist alongside this input (logical XOR).
Note: This may or may not have aliases in the final version (.disjoin, .without, .xor... etc)
Specifies that the value is allowed to be null.
Specifies a key to rename the current parameter to.
Options take the form of an object with the follow default values:
{
deleteOrig: false,
allowMult: false,
allowOverwrite: false
}
The option "deleteOrig" specifies whether or not to delete the original key of the param (effectively a permanent "move").
The option "allowMult" specifies whether or not multiple parameters can be renamed to a single key.
The option "allowOverwrite" specifies whether or not the rename function can overwrite existing keys.
Strings, by default, match JavaScript Strings. They are typically unbounded in length unless limited by interpreter. They are encoded in UTF-8 (this is true in Node.js at least). They may contain any allowable characters in the specified encoding.
The Type Registry's implementation of String also includes some builtin constraints:
Specifies that the input may be equal to '' (the empty string).
Specifies a minimum length for this input string, inclusive.
If n is not specified, it returns an Error.
If n is not a non-negative integer, it returns an Error.
Specifies a maximum length for this input string, inclusive.
If n is not specified, it returns an Error.
If n is not a positive integer, it returns an Error.
Specifies that this input may only consist of alphanumeric characters.
Specifies that this input matches the given RegExp pattern.
If pattern is not specified, it returns an Error.
If pattern is not a valid RegExp object, it returns an error.
Specifies that this input is a valid email string.
Specifies that this input is a valid Date string (locale string but also accepts unix timestamp in milliseconds).
Specifies an explicit encoding for this input string.
Warning: This may or may not be included in the final version. A better solution may be to forcibly convert from the encoding specified by enc to utf-8. However, this is not always possible (i.e. UTF-16 converting to UTF-8 would truncate a lot of characters).
Specifies that this input be a valid integer.
Specifies that this input be a valid float or double.
Specifies a minimum value for this input, inclusive.
If n is not specified, it returns an Error.
If n is not an integer, it returns an Error.
Specifies a maximum value for this input, inclusive.
If n is not specified, it returns an Error.
If n is not an integer, it returns an Error.
Boolean values accept a case-insensitive string parameter. If the value is "true", true is returned. Otherwise, false is returned.
Note: Boolean has no special methods other than those inherited from BaseType
Note Array values take the querystring form of
?cars=1&cars=2
and get converted to
{ cars: [ '1', '2' ] }
by the server.
Note: Array has no special methods other than those inherited from BaseType
Specifies allowed types for the array value to include. The values of n1, n2, ... are Type Registry constraints (usually of other types).
Specifies allowed types for the array value to exclude. The values of n1, n2, ... are Type Registry constraints (usually of other types).
Note: Object has no special methods other than those inherited from BaseType
In Hapi's routes configuration array, the routes are listed as JavaScript objects. Route objects may include an optional "query" key, the value of which should be an object. This object should associate querystring input names to validation constraints.
var queryObj = {
input_name: constraints
};
In the above code example, "input_name" must conform to typical JavaScript object key constraints (no spaces, no quotes unless escaped and surrounded by quotes, etc).
In place of "constraints", there should be a combination of constraints. The combination of constraints must be formed starting from a valid base type. The base type may be followed by zero or more pre-defined constraint functions chained consecutively. These combinations can be pre-combined into "alias" variables that may also be followed by zero or more pre-defined constraint functions chained consecutively. An example is shown below:
Base().constraint_one().constraint_two()...
BaseAlias = Base().constraint()
BaseAlias.constraint_one().constraint_two()...
Constraint functions may accept optional and arbitrary parameters.
Every call must have its own Base()
prefix. This creates a new validation object. Otherwise, it will retain settings from any prior constraints.
Each constraint is evaluated independantly and in order of chaining. In some cases, a subsequent constraint may override a prior constraint:
String.required().optional() # This input will be considered optional
String.required(false).required() # This input will be considered required
Constraints that can override modify the query validation state upon the function's evocation. The actual evaluation is performed at the end of the chain (or once the entire querystring validation is finished).
These constraint functions are special cases:
Rename is always performed at the end of the chain.
Below is an example of a schema that is likely to be used for defining a username/password constraint. Notice that 'with' is used on the 'username' to indicate that 'password' is required to appear whenever 'username' exists. Similarly, 'password' has a constraint indicating that the key 'access_token' must not exist when 'password' exists.
username: Joi.types.String().with('password'),
password: Joi.types.String().without('access_token')
Yet, in another case, a prior constraint may overrule a subsequent constraint:
Types.String().max(5).max(10) # This input cannot be larger than 5 characters
Types.String().max(3).regex(/.{0,5}/) # This input cannot be larger than 3 characters
This should apply to all other constraints that do not override.
Joi has special settings that will modify certain behaviors.
On occasion, an object must be validated which contains functions as properties. To force Joi to ignore validation on such functions, use the skipFunctions
option:
Joi.settings.skipFunctions = true;
Through the process of validation, some inputs will be converted to accommodate the various constraint functions. For example, if an input is of type Joi.Types.Number() but is defined as a string, the validator will convert to Number during validation. This does not persist and does not affect the original input.
To force Joi to save the conversion, use the saveConversions
option:
Joi.settings.saveConversions = true;
When validating an input for a specific type with lots of constraints, Joi will, by default, return error immediately upon the first error condition. In some rare cases, iterating through all of the constraint functions is actually ideal (to identify all the reasons why an input is invalid at once). To force Joi to evaluate all constraints, use the shortCircuit
option:
var S = Joi.Types.String();
S.options.shortCircuit = false;
var schema = {
nickname: S().valid('Silly').min(2)
}
schema.nickname.validate('o', null, null, errors) // => populates errors with all failing constraints
The .valid
constraint is currently exclusive - if the input is NOT one of the values passed to .valid
, the validator returns false. In the event this is too strict, use the hidden __allowOnly
option.
var S = Joi.Types.String();
S.__allowOnly = false;
var schema = {
username: S().valid('walmart')
}
schema.username.validate('test') // => this returns true
Encodings could potentially play a role in security - some strings in one encoding, when exec()'d in another encoding could execute malicious code. If this type of validation is enabled, it will likely provide little to no explicit protection for developers. Developers could unintentionally (and even worse, unknowingly) expose a significant security risk.
var Joi = require('joi');
var schema = {
username: Joi.types.String().alphanum().min(3).max(30).required(),
password: Joi.types.String().regex(/[a-zA-Z0-9]{3,30}/).required(),
};
var invalidObj = { username: 'roger' };
var validObj = { username: 'roger', password: 'pa55word' };
Joi.validate(invalidObj, schema, function (err) {
if (err) throw err;
});
Joi.validate(validObj, schema, function (err) {
if (err) throw err;
});
Executing the above code outputs the following:
Error: [ValidationError]: the value of `password` is not allowed to be undefined
var Joi = require('joi');
var schema = {
num: Joi.types.Number().required()
};
var obj = { num: '1' };
Joi.validate(obj, schema, function (err) {
if (err) throw err;
else console.log('Success!');
});
Executing the above code outputs the following:
Success!
The "null" variable is considered to be of type "object". An alias could easily be added for this type if necessary. However, for the purposes of querystring validation, this appears to be unnecessary.
Unlike null, undefined is its own type with its own special properties. For the purposes of querystring validation, any blank or indefinite inputs will appear as blank strings (""). As far as I know, there is no way to force the undefined object into the querystring. Thus, unless otherwise proven, "undefined" will not be included in the Type Registry.
FAQs
Object schema validation
The npm package joi receives a total of 9,164,622 weekly downloads. As such, joi popularity was classified as popular.
We found that joi demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 6 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
At its inaugural meeting, the JSR Working Group outlined plans for an open governance model and a roadmap to enhance JavaScript package management.
Security News
Research
An advanced npm supply chain attack is leveraging Ethereum smart contracts for decentralized, persistent malware control, evading traditional defenses.
Security News
Research
Attackers are impersonating Sindre Sorhus on npm with a fake 'chalk-node' package containing a malicious backdoor to compromise developers' projects.