specified

Type-safe typescript data specification verification

Usage no npm install needed!

<script type="module">
  import specified from 'https://cdn.skypack.dev/specified';
</script>

README

specified

Type-safe Typescript data specification verification.

Rationale

Any data flowing into a program at run-time should be checked for validity, while it also has an implicit or explicit type. Examples are: user input, http request body payloads, http response body payloads, message bus event payloads, environment variables, configuration files. The specified package allows to describe a "spec" of the data, by specifying its type and any constraints. The spec can then be used to verify the data and will automatically assign the correct Typescript type to the result.

Design goals

  • Single definition of type and validation.
  • Strictly typed and type-safe.
  • Zero dependencies.
  • Easy extendability: simple to create custom convertions and validations.

Upgrade from v0.x to v1.y

See the document UPGRADE.md for information about upgrading from an older major version to the current version of specified.

Specs

What is a spec?

A spec can describe any type of variable, but in most use cases it will describe an object or interface. Specs can be used nested inside other specs. A spec consists of a type and optionally some constraints.

Here are some examples of simple specs:

import { Type, constrain, Constraint } from "specified";

const stringSpec = Type.string;

const integerSpec = constrain(Type.number, [Constraint.number.integer]);

const interfaceSpec = Type.interface({
    propertyOne: Type.string,
    propertyTwo: integerSpec
});

The argument to the function Type.interface is called a schema. A schema describes all the known properties of an interface or object model. The value of each property in a schema is again a spec, describing the type and constraints that the value of that property in the data should have. See the section Schemas for all information regarding schemas.

A full list of all built-in spec types and constraints can be found below in the Reference section. Custom types and constraints can be created in case the built-in ones don't fulfill your needs.

Some of the built-in spec types are designed to (potentially) transform the input data into a different type. For example, Type.numeric will accept any input that can be converted to a 'number' and when it can convert it then it always returns a value of type 'number'.

Verifying data

A spec can be used to check whether some data is valid according to that spec. For this purpose the verify function can be used, by supplying it both the spec and the data. The output of the verification contains two fields: err which indicates a validation failure error or null in case of a successful validation; and value() which is a function that either returns a version of the data that corresponds to the spec or it throws a validation error in case of a validation failure. When the err field of the output is null, then value() will be guaranteed not to throw an error.

So, there are two ways to use the output of the verification. One way is to first check for a validation failure error and then safely use the value:

import { Type, verify } from "specified";

const verificationResult = verify(Type.string, 123);
if (verificationResult.err) {
    console.log("Validation failure!");
}
else {
    console.log(`The verified value: ${verificationResult.value()}`);
}

The other way is to make sure that a potential validation error is caught:

import { Type, verify, ValidationError } from "specified";

try {
    const verifiedValue = verify(Type.string, 123).value();
    console.log(`The verified value: ${verifiedValue}`);
}
catch (err) {
    if (err instanceof ValidationError) {
        console.log("Validation failure!");
    }
}

Of course the validation failure error contains more details about what exactly went wrong when verifying the data. The structure of the validation failure error will be discussed later on in this document.

Typescript type of the validated data

The type of the output value of the data verification is inferred by typescript, corresponding to the type indicated by the used spec. Some examples:

import { Type, verify } from "specified";

const myStringValue1: string = verify(Type.string, 123).value();
const myStringValue2: string = verify(Type.number, 123).value(); // COMPILE ERROR: Type 'number' is not assignable to type 'string'.

The useful generic type helper VerifiedType<...> will construct the typescript type of the verification output value corresponding to the provided spec. Some examples:

import { Type, VerifiedType } from "specified";

type verifiedValueType1 = VerifiedType<typeof Type.boolean>; // This type will be 'boolean'.
type verifiedValueType2 = VerifiedType<typeof Type.array(Type.number)>; // This type will be 'number[]'.
type verifiedValueType3 = VerifiedType<typeof Type.object({ x: Type.string })>; // This type will be '{ x: string; }'.

const pointSpec = Type.interface({ x: number, y: number });
interface Point extends VerifiedType<typeof pointSpec> {}

Spec constraints

Besides the type of the data, a spec can optionally describe constraints that the data should adhere to in order to successfully validate.

Only constraints that match with the type of the spec can be added to the spec. Some constraints are generic or can match with multiple spec types.

Constraints do not influence the verification output value. They merely check that the data adheres to the constraint.

Some examples of constraints:

import { Type, constrain, Constraint } from "specified";

const nonEmptyStringSpec = constrain(Type.string, [Constraint.string.length({ min: 1 })]);
const positiveIntegerSpec = constrain(Type.number, [
    Constraint.number.integer,
    Constraint.number.above(0)
]);

Combining specs

A union of two or more specs can be created with the either(spec1, spec2, ... , specN) function, resulting in a new spec, which succeeds verification if the data value is accepted by one of the provided specs. The resulting typescript type is the union of verification result types of the provided specs. For example, either(Type.string, Type.number) results in the type string | number. See the either reference section for more information.

Validation failure

When verifying data according to a certain spec and the data does not conform to the type or the constraints of that spec, then it will result in a validation failure. The failure will be returned in the err field of the verification result.

The data structure of the validation failure conforms to the ValidationFailure interface. This interface has the following properties:

  • code: A unique identifier string that corresponds with the reason of the occurred failure. Each spec type and each spec constraint has one or more unique reasons for which it can fail, each with a unique error code. When programmatically checking for a specific error then this code should be used.
  • value: The data value for which the failure occurred. For a nested spec only the nested value will be part of the validation failure instance.
  • allowed: Either an enumeration of all data values allowed by the spec, or a description of the data specification (for example: the regular expression in case of the Constraint.string.regex constraint). Not all failure reasons have this field filled in.
  • message: A human readable message describing the failure reason. This message is not guaranteed to be exactly the same across different versions of specified. Therefore the code field should be used when programatically checking for a specific failure.
  • key: For failures in a nested spec, this field indicates under which index number (for Type.array and Type.tuple) or key string (for Type.object, Type.interface and Type.map) the failed spec is nested within the parent spec. For non-nested spec this field is not applicable.
  • nestedErrors: In case of nested failure this field contains an array with one or more object conforming to the ValidationFailure interface. Note that nested failures can itself also contain nested failures.

The exported FormatValidationFailure contains functions that format an instance of ValidationFailure into a more human readable JSON format. This is especially useful for failures containing nested errors.

Full example

A simple but full example of using specified:

import {Type, constrain, Constraint, verify} from "specified";

const productSpec = Type.interface({
    description: Type.string,
    price: constrain(Type.number, [Constraint.number.above(0)])
});

interface Product extends VerifiedType<typeof productSpec> {}

const data = JSON.parse('{"description":"Peanut butter","price":3.50}');
const verificationResult = verify(productSpec, data);
if (verificationResult.err) {
    console.log(FormatValidationFailure.generateErrorPathList(verificationResult.err));
}
else {
    const product: Product = verificationResult.value();
    console.log(product);
}

Reference

Spec modifiers

constrain

The constrain(spec, [constraint1, constraint2, ...]) function adds one or more constraints to a spec. When evaluating data against the spec then these constraints are evaluated and will cause the evaluation to fail in case the data does not adhere to the constraint.

adjust

The function adjust(spec, localOptionsToAdjust) creates a clone of the spec, with one or more adjustments on the local options of the spec. For example adjust(Type.object({}), { strict: false }).

optional

The function optional(spec) only applies to a spec which is used as a schema attribute. It causes the attribute to become optional, so that the evaluation of the schema does not fail when this attribute is not present.

either

The function either(spec1, spec2, ...) combines multiple specs to create a new spec. Verification of this new spec will succeed if either one or more of the specs in the combination succeeds. The specs in the combination are evaluated in the order they are passed to the either function.

Type

Type.string

The spec type Type.string accepts any data of type string. It results in the typescript type string.

Verification of a spec with this type will result in a failure with error code type.string.not_a_string in case the data value is not a string.

Type.number

The spec type Type.number accepts any data of type number, including number values such as NaN and Infinity. Constraints can be used to prevent these kind of values. It results in the typescript type number.

Verification of a spec with this type will result in a failure with error code type.number.not_a_number in case the data value is not a number.

Type.boolean

The spec type Type.boolean accepts data of the type boolean, so basically only the values true and false. It returns the typescript type boolean.

Verification of a spec with this type will result in a failure with error code type.boolean.not_a_boolean in case the data value is not a boolean.

Type.unknown

The spec type Type.unknown accepts any data, but returns unknown as its data type. This spec type will never cause a validition failure.

Use cases for this spec type include:

  • checking that a property on an object exists, but it doesn't matter what its value is.
  • splitting a complex spec into multiple parts, where using unknown for the parts of the data that will be verified using a different spec.

Type.null

The spec type Type.null accepts only the value null and results in the typescript type null.

Verification of a spec with this type will result in a failure with error code type.null.not_null in case the data value is something else than null.

Type.symbol

The spec type Type.symbol accepts any symbol and results in the typescript type symbol.

Verification of a spec with this type will result in a failure with error code type.symbol.not_a_symbol in case the data value is not a symbol.

Type.literal

The spec type Type.literal(objectWithLiteralValuesAsKeys) accepts only the literal string values which are provided as keys in its type parameter. The values of the type parameter object should all be 1 or true and have no functional meaning. An example:

const sizeSpec = Type.literal({ "tiny": 1, "small": 1, "regular": 1, "big": 1, "huge": 1 });

The typescript type of the verification output value is the union type of all literal strings. In the example that typescript type is "tiny" | "small" | "regular" | "big" | "huge".

Verification of a spec with this type will result in a failure with error code type.literal.incorrect_literal in case the data value does not equal one of the specified literals.

The Type.literal spec type is useful to describe "enumerations". It can be used for implementing the discriminated union pattern.

Type.literalValue

The spec type Type.literalValue(acceptedValues) accepts any of the values provided in its type parameters. It results in the typescript type that is the type union of the accepted values.

It is advisable to only use actual literal values only, which for example allows typescript to help indicating which are the possible values or to help checking that all possible values are handled in a switch-statement.

Verification of a spec with this type will result in a failure with error code type.literalValue.incorrect_literal_value in case the data value does not equal one of the specified accepted values.

The Type.literalValue can be used for implementing the discriminated union pattern.

Type.array

The spec type Type.array(elementSpec) accepts arrays of which the elements are accepted by the elementSpec spec. The resulting typescript type is an array of the result type of its elements. For example Type.array(Type.number) will result in the type number[].

There are 2 local options and 1 global option that can adjust the behavior of evaluation of the Type.array spec. The local options can be adjusted using the adjust function. Local options are not applied recursively. Global options can be provided when invoking the verify function.

  • failEarly is both a global and local boolean option that causes the evaluation of the spec to fail directly when one of the evaluation of one of its elements fails when set to true. At most 1 nested element error will be reported in that case. The default value false lets the evaluation of all elements be finalized in case of evaluation failures of one or more of the elements. All nested element errors will be returned in this case. Note: the local option takes precedence over the global option.
  • skipInvalid is a local boolean option with default value false, which causes the evaluation to skip elements that are not accepted by the element spec when set to true. Invalid elements will not be part of the evaluation result and their errors are ignored.

Verification of a spec with this type can result in a the following failures:

  • Error code type.array.not_an_array in case the data value is not an array.
  • Error code type.array.invalid_elements if the evaluation of one or more elements returns a failure. For each failed element it will contain a nested error with error code type.array.invalid_element and the element index in the key field, which in turn contains the element's failure as a nested error.

Type.tuple

The spec type Type.tuple(...specs) accepts a tuple (an array with a fixed length) of which each element is accepted by the provided spec at the same position in the tuple. The resulting typescript type is a tuple with the result type of each of it element specs at the same position. For example, the spec Type.tuple(Type.string, Type.number, Type.boolean) has the result type [string, number, boolean]. It will accept the data ["abc", 123, true], but it will fail on the data [123, "abc", true] and ["abc", 123] and ["abc", 123, true, "extraElement"].

The behavior of the evaluation of Type.tuple can be adjusted with the global and local (using the adjust function) boolean option failEarly to stop the evaluation on the first encountered failure when evaluating its elements when set to true. It will result in at most 1 nested error in that case. The default value is false, which can result in multiple element failures. The local option for failEarly takes precedence over the global option, and it does not work recursively.

Verification of a spec with this type can result in the following failures:

  • Error code type.tuple.not_a_tuple in case the data is not a tuple (i.e. an array).
  • Error code type.tuple.incorrect_length in case the data is an array, but its length is not equal to the number of specified element specs.
  • Error code type.tuple.invalid_elements if the evaluation of one or more elements returns a failure. For each failed element it will contain a nested error with error code type.tuple.invalid_element and the element index in the key field, which in turn contains the element's failure as a nested error.

Type.object and Type.interface

The spec types Type.object(schema) and Type.interface(schema) accepts objects of which the data properties are valid according to the spec provided in the schema at the attribute with the same name. For example the spec Type.object({ x: Type.number, y: Type.string }) accepts the data { x: 123, y: "abc" }. The verification result typescript type is an object with a property named after each attribute in the schema, with the type of each property's value being the result type of the spec corresponding to the attribute in the schema.

The only difference between the spec types Type.object(schema) and Type.interface(schema) is that by default Type.object is strictly not allowing attributes to be present in the data that are not part of the schema, while Type.interface allows (but ignores) unspecified attributes in the data. Type.interface(schema) is equivalent to adjust(Type.object(schema), { strict: false }).

There are 2 local options and 1 global option that can adjust the behavior of evaluation of the Type.object and Type.interface spec. The local options can be adjusted using the adjust function. Local options are not applied recursively. Global options can be provided when invoking the verify function.

  • failEarly is both a global and local boolean option that causes the evaluation of the spec to fail directly when one of the evaluation of one of its attribute data values fails when set to true. At most 1 nested attribute error will be reported in that case. The default value false lets the evaluation of all attributes be finalized in case of evaluation failures of one or more of the attributes. All nested attribute errors will be returned in this case. Note: the local option takes precedence over the global option.
  • strict is a local boolean option with default value true for Type.object and false for Type.interface. When set to false, it causes the evaluation to ignore properties that are not present as attributes in the schema. When set to true, properties not present as attributes in the schema will cause the evaluation to fail.

Verification of a spec with one of these types can result in the following failures:

  • Error code type.object.not_a_regular_object or type.interface.not_a_regular_object in case the data value is not an object, or when it's null or an array instance.
  • Error code type.object.extra_attribute or type.interface.extra_attribute when evaluating in strict mode and the data object contains a key that is not present as an attribute in the schema.
  • Error code type.object.missing_attribute or type.interface.missing_attribute in case a mandatory attribute (i.e. not declared as optional) is missing in the data object.
  • Error code type.object.invalid_attribute_data or type.interface.invalid_attribute_data if the evaluation of one or more of the object properties values returns a failure. For each failed attribute it will contain a nested error with error code type.object.invalid_attribute or type.interface.invalid_attribute and the attribute name in the key field, which in turn contains the property value's failure as a nested error.

Type.map

The spec type Type.map(keySpec, valueSpec) accepts objects of which the keys are valid according to the keySpec (which must have a string result type) and the values are valid according to the valueSpec. The verification result's typescript type is an object with string keys and the values with the resulting type of the valueSpec verification: { [key: string]: VerifiedType<typeof valueSpec> }.

With the following global and local options the behavior of the Type.map spec can be adjusted:

  • failEarly is both a global and local boolean option that causes the evaluation of the spec to fail directly on the first failure encountered when verifying the data keys and values when set to true. At most 1 nested error will be reported in that case. The default value false lets the evaluation of all keys and values be finalized in case of evaluation failures of one or more keys or values. All nested attribute errors will be returned in this case. Note: the local option takes precedence over the global option.
  • skipInvalidKeys is a local boolean option with default value false. It causes the evaluation to skip keys that are not accepted by the key spec when set to true. Invalid keys will not be part of the evaluation result and their errors are ignored. Their corresponding values will not be evaluated.
  • skipInvalidValues is a local boolean option with default value false. It causes the evaluation to skip values that are not accepted by the value spec when set to true. Invalid values will cause its key to not be part of the evaluation result and their errors are ignored.

Verification of the Type.map spec can result in the following failures:

  • Error code type.map.not_a_regular_object when the data value is not an object or when it's null or an array instance.
  • Error code type.map.invalid_data in case the evaluation of one or more keys or values fails. It will contain nested errors with either the type.map.invalid_key or type.map.invalid_value error code and the key field set to the failing data key, which in turn contains a nested error with the failure of the key or value evaluation.

Type.instance

The spec type Type.instance(ctor) accepts objects that are class instances create through the ctor constructor function. The resulting typescript type is the instance type of the class.

Verification of the Type.instance spec can result in a failure with the error code type.instance.not_an_instance_of in case the data value is not an instance of the class of which ctor is the constructor.

Example:

class MyClass {}
const myClassSpec = Type.instance(MyClass);
const myInstance = new MyClass();
const myVerifiedInstance = verify(myClassSpec, myInstance).value();

Type.numeric

The spec type Type.numeric accepts every value that can convert into a finite number. The resulting typescript type of the verification is number. These are examples of accepted values: 123, "-123", true (which results in value 1).

Verification of this type can result in a failure with error code type.numeric.not_a_finite_number when the data value does not represent a finite number.

Type.booleanKey

The spec type Type.booleanKey(keys, options) can operate in two modes, depending on whether only truthy keys are provided or also falsy keys. The resulting typescript type is always a boolean.

  • If only truthy keys are provided, for example Type.booleanKey({ truthy: ["yes", "true"] }), then the evaluation will result in the value true only if the data value converted to a string exactly equals one of the truthy keys, otherwise the result will be false.
  • If both truthy and falsy values are provided, for example Type.booleanKey({ truthy: ["Yes"], falsy: ["No"] }), then the evaluation will result in either true or false when the data value converted to a string is one of the provided truthy or falsy keys respectively. If the data value is not one of the specified keys, then the verification will result in a failure with error code type.booleanKey.invalid_key.

The option parameter of the spec is optional and can contain the optional caseInsensitive boolean option, which is by default false. When set to true, the comparison between the data value and the keys is done in a case-insensitive way, by converting both the key and data value to lower case characters.

Constraint

The specified distribution contains a number of built-in constraints that can be applied to a spec of the same type using the constrain function.

The constraints are divided into groups, according to their type. Some constraints can apply to multiple types.

Constraint.generic.oneOf

The generic constraint Constraint.generic.oneOf(valueOptions) checks whether the evaluated value from the spec equals one of the values in the valueOptions array. Note that the spec result type should be compatible with the value options' type.

Verification of a spec with this constraint can result in a failure with error code constraint.generic.oneOf.unknown_value.

Constraint.number.integer

The constraint Constraint.number.integer applies to number values, and checks whether the number value is an integer.

Verification of a spec with this constraint can result in a failure with error code constraint.number.integer.

Constraint.number.finite

The constraint Constraint.number.finite applies to number values, and checks whether the number value is finite, i.e. that it is not equal to NaN or Infinite or -Infinite.

Verification of a spec with this constraint can result in a failure with error code constraint.number.finite.

Constraint.number.above

The constraint Constraint.number.above(lowerLimit) applies to number values. It checks whether the data value is higher than the provided lower limit.

Verification of a spec with this constraint can result in a failure with error code constraint.number.above.

Constraint.number.below

The constraint Constraint.number.below(upperLimit) applies to number values. It checks whether the data value is lower than the provided upper limit.

Verification of a spec with this constraint can result in a failure with error code constraint.number.below.

Constraint.number.atLeast

The constraint Constraint.number.atLeast(lowerLimit) applies to number values. It checks whether the data value is higher than or equal to the provided lower limit.

Verification of a spec with this constraint can result in a failure with error code constraint.number.atLeast.

Constraint.number.atMost

The constraint Constraint.number.atMost(upperLimit) applies to number values. It checks whether the data value is lower than or equal to the provided upper limit.

Verification of a spec with this constraint can result in a failure with error code constraint.number.atMost.

Constraint.string.notEmpty

The constraint Constraint.string.notEmpty applies to string values. It checks whether the value is not an empty string.

Verification of a spec with this constraint can result in a failure with error code constraint.string.notEmpty.

Constraint.string.length

The constraint Constraint.string.length({ min, max }) applies to string values. It checks whether the string value's length is within the required range.

The range can be defined with

  • only the minimum length, for example Constraint.string.length({ min: 3 }),
  • only the maximum length, for example Constraint.string.length({ max: 10 }),
  • both a minimum and maximum, for example Constraint.string.length({ min: 3, max: 10 }).

Verification of a spec with this constraint can result in a failure with error code constraint.string.length.too_short or constraint.string.length.too_long.

Constraint.string.regex

The constraint Constraint.string.regex(pattern) applies to string values. It checks that the string value matches the regular expression pattern.

Verification of a spec with this constraint can by default result in a failure with error code constraint.string.regex. But a custom error code and custom error message can be provided as a second parameter to this constraint, to override them. For example: Constraint.string.regex(/^[a-z]+$/, custom: { errorMessage: "Not a lowercase word.", errorCode: "Constraint.string.lowercaseWord" }).

Constraint.string.startsWith

The constraint Constraint.string.startsWith(prefix) applies to string values. It checks that the string value starts with the provided prefix string.

Verification of a spec with this constraint can result in a failure with error code constraint.string.startsWith.

Constraint.string.endsWith

The constraint Constraint.string.endsWith(suffix) applies to string values. It checks that the string value ends with the provided suffix string.

Verification of a spec with this constraint can result in a failure with error code constraint.string.endsWith.

Constraint.map.size

The constraint Constraint.map.size({ min, max }) applies to the map type. It checks whether the number of keys in the map is within the required range.

The range can be defined with

  • only the minimum length, for example Constraint.map.size({ min: 3 }),
  • only the maximum length, for example Constraint.map.size({ max: 10 }),
  • both a minimum and maximum, for example Constraint.map.size({ min: 3, max: 10 }).

Verification of a spec with this constraint can result in a failure with error code constraint.map.size.too_small or constraint.map.size.too_large.

Constraint.array.length

The constraint Constraint.array.length({ min, max }) applies to array values. It checks whether the array's length is within the required range.

The range can be defined with

  • only the minimum length, for example Constraint.array.length({ min: 3 }),
  • only the maximum length, for example Constraint.array.length({ max: 10 }),
  • both a minimum and maximum, for example Constraint.array.length({ min: 3, max: 10 }).

Verification of a spec with this constraint can result in a failure with error code constraint.array.length.too_short or constraint.array.length.too_long.

Constraint.array.includes

The constraint Constraint.array.includes(needle) applies to arrays. It checks that the needle value is included in the array. Note that it only compares value and not nested structures, therefore an object instance is only considered to be "included" if the exact same object reference is part of the array.

Verification of a spec with this constraint can result in a failure with error code constraint.array.includes.

Constraint.array.unique

The constraint Constraint.array.unique(equalsFunc) applies to arrays. It checks whether all elements in the array are equal according to the equalsFunc function. The default equalsFunc does a simple === comparison.

Verification of a spec with this constraint can result in a failure with error code constraint.array.unique.

NOTE: In the worst case scenario, this constraint does 1/2 * (N^2 - N) calls to the equalsFunc function, where N is the array length.

Spec definitions

definitionOf

The function definitionOf(spec) returns the definition of the provided spec. The definition contains various information about the spec. It conforms to the following interface:

interface SpecDefinition {
    readonly type: string;
    readonly nested?: { [key: string]: SpecDefinition };
    readonly alias?: string;
    readonly constraints?: ConstraintDefinition[];
    readonly adjustments?: object;
    readonly flags?: string[];
    readonly defaultValue?: unknown;
    readonly descriptions?: { [attr: string]: string };
}

For example, definitionOf(Type.array(constrain(Type.number, [Constraint.number.above(0)]))) results in this definition:

{
    "type": "array",
    "nested": {
        "element": {
            "type": "number",
            "constraints": [
                {
                    "name": "above",
                    "settings": {
                        "lowerLimit": 0
                    }
                }
            ]
        }
    }
}

alias

The alias(aliasName, spec) function creates a new spec with the alias name string set or overwritten in the spec's definition.

Aliases can be extracted from a spec definition using the extractAliases function.

extractAliases

The extractAliases(definition) extracts aliases from the provided definition. The result is the list of aliased sub-definitions together with the definition in which all aliased sub-definitions are replaced by the alias name.

Example:

const integerSpec = alias("integer", constrain(Type.number, [Constraint.number.integer]));
const coordinatesSpec = Type.object({ x: integerSpec, y: integerSpec });
const coordinatesSpecDef = definitionOf(coordinatesSpec);
const coordinatesSpecDefWithExtractedAliases = extractAliases(coordinatesSpecDef);

This will result in:

{
    "definition": {
        "type": "object",
            "nested": {
                "x": {
                    "alias": "integer"
                },
                "y": {
                    "alias": "integer"
                }
            }
    },
    "aliases": {
        "integer": {
            "type": "number",
            "constraints": [
            {
                "name": "integer"
            }
            ],
            "alias": "integer"
        }
    }
}

Validation failures

ValidationFailure

The ValidationFailure interface describes the error as returned by the verify function.

interface ValidationFailure {
    code: string;
    value: unknown;
    allowed?: unknown;
    message: string;
    key?: ValidationErrorKey;
    nestedErrors?: ValidationFailure[];
}

ValidationError

The ValidationError class implements the ValidationFailure interface. An new instance of this class is thrown by the .value() function of the verification result in case of a validation failure.

Note that for forwards-compatibility reasons it is discouraged to rely on the methods of this class, but instead to treat it as an instance of the ValidationFailure instance only. The class should then only be used in a catch handler to detect the type of error, as in this example:

try {
    ...
    verifiedData = verify(spec, data).value();
    ...
}
catch (err) {
    if (err instanceof ValidationError) {
        const validationFailure: ValidationFailure = err;
        // Do something with the validation failure here.
    }
}

FormatValidationFailure

The FormatValidationFailure namespace contains functions to convert an object with the ValidationFailure interface into a reportable representation.

generateReportJson

The function FormatValidationFailure.generateReportJson(err, options) generates a JSON report with the error details.

Supported options are:

  • include contains boolean switches for including extra details in the report. The supported switches are:
    • include.message to include the message in the report. Default value: true.
    • include.code to include the error code in the report. Default value: false.
    • include.value to include the data value on which the spec evaluation failed. Default value: false. Note: be careful to not include sensitive data in reports.
    • include.allowed to include a description of which data values are allowed by this spec.
generateErrorPathList

The function generateErrorPathList(err, options) generates a list of all (nested) "paths" in the validation which caused a failure. Note that it depends on the spec options whether all paths were evaluated or that it already returned upon the first failure.

Supported options are:

  • include contains boolean switches for including extra details in the report. The supported switches are:
    • include.message to include the message in the report. Default value: true.
    • include.code to include the error code in the report. Default value: false.
    • include.value to include the data value on which the spec evaluation failed. Default value: false. Note: be careful to not include sensitive data in reports.
    • include.allowed to include a description of which data values are allowed by this spec.

Here is an example of a path list:

[
    {
        "msg": "Data has attribute that is not part of the strict schema: \"b\".",
        "path": ["objArray", 1, "b"]
    },
    {
        "msg": "Missing attribute: \"a\".",
        "path": ["objArray", 1, "a"]
    },
    {
        "msg": "Not a number.",
        "path": ["objArray", 2, "a"]
    },
    {
        "msg": "Not an array.",
        "path": ["arrayMap", "y"]
    },
    {
        "msg": "Not a number.",
        "path": ["arrayMap", "z", 1]
    }
]

Verification

verify

The verify(spec, data, globalOptions, verifyOptions) function evaluates whether the provided data is valid according to the provided spec.

See the section on verifying data on how to process the result of this function.

By default globalOptions is empty. But it may contain the following options, which will then be applied to all (nested) specs during evaluation:

  • failEarly: Stop the evaluation upon the first encountered failure.

The verifyOptions contain the following options:

  • errorClass: The error class to use when throwing an error from the .value() function of the verification result. Default value: ValidationError. The used class must have a constructor functionwith this signature: function (msg: string, err: ValidationFailure): ValidationFailure.

VerifiedType

The VerifiedType<Spec> typescript generic type corresponds to the verification result type of the provided spec.

Example usage:

const spec = Type.array(Type.number);
type SpecResultData = VerifiedType<typeof spec>; // Equals 'number[]'.

Schemas

A schema describes an object or interface by its attributes and their value specifications. The attributes can be declared optional.

A simple example:

const exampleSchema = {
    numattr: Type.number,
    optStrAttr: optional(Type.string)
};

const exampleObjectSpec = Type.object(exampleSchema);
type ExampleObjectType = VerifiedType<typeof exampleObjectSpec>;
// ExampleObjectType corresponds to { numAttr: number, optStrAttr?: string }

Attribute descriptions

A description can be added to a schema attribute, which will appear in the definition of the spec under the descriptions key.

An example:

const personSpec = Type.interface({
    name: {
        ...Type.string,
    description: "The person's name."
    },
    age: {
        ...Type.number,
        description: "The age in years."
    }
});

Reusable "base" schemas

A schema can be used as a "base" schema for multiple specs, when declared in a separate variable.

Here's an example:

const shapeBaseSchema = {
    color: Type.literal({ blue: 1, red: 1 }),
    filled: Type.boolean
};

const circleShapeSpec = Type.object({
    ...shapeBaseSchema,
    shapeType: Type.literal({ circle: 1 }),
    radius: Type.number
});

const rectangleShapeSpec = Type.object({
    ...shapeBaseSchema,
    shapeType: Type.literal({ rectangle: 1 }),
    width: Type.number,
    height: Type.number
});

Note that it is important that the base schema's typescript type is inferred, for the specs to be strongly typed.

Discriminated unions

In the base schema example the shapeType attribute can be used as a discriminated union in case a generic shape spec is needed:

const shapeSpec = either(circleShapeSpec, rectangleShapeSpec);
const shape = verify(shapeSpec, shapeData).value();
if (shape.shapeType === "circle") {
    console.log(`Circle radius: ${shape.radius}`); // NOTE: 'radius' is strongly-typed here!
}

The following example shows how it is possible to add an unknown type to the union:

const otherShapeSpec = Type.object({
    ...shapeBaseSchema,
    shapeType: optional(Type.literal({}))
});

const shapeSpec = either(circleShapeSpec, rectangleShapeSpec, otherShapeSpec);

Extendability

It is relatively easy to extend the specified library with custom spec types and spec constraints, in case the types and constraints which are delivered with this package do not fulfill your needs.

Types and constraints are decoupled from the core of specified through the use of interfaces. This means that there is no dependency needed on the specified library to create a custom type or constraint. Many examples of type and constraint implementations can be found in the source code of the specified package.

Custom types

A spec type is the basic part of a spec. It consists of an object structure that contains meta information, but most importantly an evaluation function. The evaluation function takes a value of an unknown type, then performs checks on the value and then guarantees the type of the returned value, or returns a validation-failure instead.

There are three kinds of spec types:

  • Simple: The evaluation function just checks for a type. There are no settings that modify the behavior of the evaluation. Example: Type.number.
  • Configurable: The actual spec type is the result of a function call, which has parameters to modify the behavior of the evaluation. Example: Type.literal({ accepted: 1, literals: 1 }).
  • Higher-order: The spec type covers a data structure that contains nested specs. Example: Type.array(nestedSpec).

The object structure of a type contains the following properties:

  • version: The version of the type interface that is used. This enables future changes to this interface, while maintaining backwards compatibility. Currently only the literal value 1 is supported.
  • definition: The definition of the spec type. See spec definitions. It always contains the type property, that should be unique within a project. For 'configurable' spec types it should contain the settings property, which contains the values set for the configuration parameters. For 'higher-order' spec types, it should contain the nested property which contains the definition of the nested spec(s).
  • eval: The evaluation function that determines whether the supplied value conforms to the type. Next to the value parameter it has a second options parameter that may contain local and global options to be applied during evaluation. See adjust and verify for explanations of local and global options respectively. The evaluation function returns either { err: { ... } } in case of a validation failure, where the err property conforms to the ValidationFailure interface. Or, upon successful evaluation, it returns { err: null. value: value } , where the value has the correct typescript type. In case the value contains mutable data, it is best to clone the output value of the evaluation function, because a change in the original data may render a successful evaluation obsolete.

Here's an illustrative example of a spec type implementation, specifically the Type.number type:

const type_number = {
    version: 1 as 1,
    definition: {
        type: "number"
    },
    eval: (value: unknown) => {
        if (typeof value !== "number") {
            return { err: { code: "type.number.not_a_number", value, message: "Not a number." } };
        }
        return { err: null, value };
    }
};

After defining a custom spec type in your project, you can use it like any other spec type.

Custom constraints

A spec constraint consists (much like a spec type) of an object structure with meta information and an evaluation function. It differs from a spec type in two ways: it doesn't return an output value, only a potential failure, and it takes a typed input parameter. The input parameter type determines on which spec types the constraint can be put, as it needs to be compatible with the spec type's output value type.

There are two kinds of spec constraints:

  • Simple: The evaluation function just checks whether the value meets the constraint. There are no settings that modify the behavior of the evaluation. Example: Constraint.number.integer.
  • Configurable: The actual spec constraint is the result of a function call, which has parameters to modify the behavior of the evaluation. Example: Constraint.string.length({ min, max }).

The object structure of a type contains the following properties:

  • version: The version of the type interface that is used. This enables future changes to this interface, while maintaining backwards compatibility. Currently only the literal value 1 is supported.
  • definition: The definition of the spec constraint. It has a mandatory name string property, which should be unique within your project. In case of a 'configurable' constraint, it should also contain the settings property, indicating the values for the configuration parameters.
  • eval: The evaluation function that determines whether the supplied value meets the constraint. The evaluation function returns either { err: { ... } } in case of a validation failure, where the err property conforms to the ValidationFailure interface. Or, upon successful evaluation, it returns { err: null }.

As an example, here's the implementation of the "integer" constraint for the number type:

const constraint_integer = {
    version: 1 as 1,
    definition: {
        name: "integer"
    },
    eval: (value: number) => {
        return { err: value % 1 !== 0 ? {
            code: "constraint.number.integer",
            value,
            message: "Not an integer."
        } : null };
    }
};

Contributors