npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ololoepepe/validator

v0.4.1

Published

Data validator

Readme

Validator

Data validator

Install

npm install @ololoepepe/validator

The package is ESM-only and ships three builds, one per consumer:

| Consumer | Artefact | How it is reached | | --- | --- | --- | | Node | dist/node — untranspiled ES modules plus declarations | import {createValidator} from '@ololoepepe/validator' | | A bundler targeting browsers | dist/browser — one transpiled file per module, so an unused export can be dropped | the browser export condition, resolved automatically | | A plain <script src="…"> | dist/umd — a single UMD bundle exposing the global validator | @ololoepepe/validator/umd |

The browser build targets browsers released since 2020 and carries its polyfills as pure imports from core-js-pure, so nothing is patched onto the globals.

The package is written in TypeScript and ships its own declarations; nothing extra is needed to get types. See TypeScript for what is inferred from a template.

Example

import Crypto from 'node:crypto';

import {createValidator, ValidatorError} from '@ololoepepe/validator';

import DB from '../lib/database.js';

const UserValidator = createValidator({
  type: 'object',
  fields: {
    nickname: {
      type: 'string',           // Nickname must be a string
      minLength: 4,             // with a minimum length of 4
      maxLength: 20,            // and maximum length of 20
      regexp: /^[a-zA-Z0-9_]+$/ // consisting of latin letters, digits and underscores
    },
    password: {
      type: 'string', // password must be a string
      minLength: 8,   // with a minimum length of 8
      maxLength: 32,  // and maximum length of 32
      validateAfter: (pwd: string) => {
        // A password must contain at least one digit
        if (!/[0-9]/.test(pwd)) {
          throw new ValidatorError('PASSWORD_HAS_NO_DIGITS');
        }

        // A password must contain at least one lowercase letter
        if (!/[a-z]/.test(pwd)) {
          throw new ValidatorError('PASSWORD_HAS_NO_LOWERCASE_LETTERS');
        }

        // A password must contain at least one uppercase letter
        if (!/[A-Z]/.test(pwd)) {
          throw new ValidatorError('PASSWORD_HAS_NO_UPPERCASE_LETTERS');
        }
      },
      transformAfter: async (pwd: string) => {
        const salt = await DB.from('salt').select('value').where('type', '=', "'password'");

        // A password must be hashed after validation
        return Crypto.createHash('sha1').update(pwd).update(salt).digest('hex');
      }
    },
    kittens: {
      type: 'number',
      minValue: 0,
      // A value may be passed as a string, and it will be converted to number before any validation
      transformBefore: (count: string) => Number(count)
    }
  }
});

const result = await UserValidator.validate({
  nickname: 'Vasia',
  password: 'pwd123PWD',
  kittens: 1
});

console.log(result);

/* {
  ok: true,
  value: {
    nickname: 'Vasia',
    password: 'f3e4c83aa7c6a1da18f1facf63dcdf21c3f8a881',
    kittens: 1
  }
} */

console.log(await UserValidator.validate({nickname: 'Petia)))', password: 'pwd321PWD', kittens: 0}));

/* {
  ok: false,
  error: {
    code: 'INVALID_STRING',
    data: 'Petia)))',
    path: ['nickname'],
    info: {}
  }
} */

console.log(await UserValidator.validate({nickname: 'Slava', password: '123', kittens: '0'}));

/* {
  ok: false,
  error: {
    code: 'INVALID_STRING_LENGTH',
    data: '123',
    path: ['password'],
    info: {expectedMinLength: 8, length: 3}
  }
} */

Only the failing branch carries an error, and only the succeeding branch carries a value, so the two are never both present:

const result = await UserValidator.validate(payload);

if (!result.ok) {
  return reply.code(400).send({code: result.error.code, path: result.error.path});
}

return createUser(result.value);

When throwing suits the call site better, use parse:

const user = await UserValidator.parse(payload); // rejects with a ValidatorError

API

createValidator(template, {customTypes = [], maxDelayMsecs = 1} = {})

Creates a validator. Accepts validation template, an object in the following form:

{
  type: <type>,
  missingStrategy: [MissingStrategy], // optional
  extraStrategy: [ExtraStrategy], // optional
  defaultValue: [default_value], // optional, only required if missingStrategy is MissingStrategy.Default
  values: Array, // optional, an array of allowed values, only valid for types 'string', 'boolean', 'number'
  validateBefore: (data, options) => {}, // optional, a custom validation function called before other validators
  validateAfter: (data, options) => {}, // optional, a custom validation function called after other validators
  transformBefore: (data, options) => {}, // optional, a custom function to transform the value before validation
  transformAfter: (data, options) => {}, // optional, a custom function to transform the value after validation
  // ...fields specific for the <type>
}

validateBefore validates data after checking the type, but before any default validators. validateAfter validates data after default validators, just before transformAfter.

Every hook may be synchronous or async; whatever it returns is awaited. transformBefore, transformAfter and a custom type's validate replace the value with what they return; validateBefore and validateAfter only inspect it, and their return value is ignored.

values must contain no duplicates, and its items must be of the template's own type — both are checked when the validator is created, not when data arrives.

The option set is closed

A template may only carry the options its type actually reads. An option that is misspelled, or that belongs to another type, is rejected when the validator is created:

createValidator({type: 'string', maxLenght: 254});
// Error: Validator: Unknown .maxLenght

createValidator({type: 'number', maxLength: 3});
// Error: Validator: Unknown .maxLength   (maxLength is a string and array option)

Nothing would ever read such an option, so a template carrying one validates less than it was written to — silently, which is the worst way for a validator to fail. TypeScript reports the same mistake at the call site, on the offending option and at any depth:

createValidator({
  type: 'object',
  fields: {
    nickname: {type: 'string', maxLenght: 20}
    //                         ~~~~~~~~~ Type 'number' is not assignable to type 'never'
  }
});

hint is part of that set only inside variants, which is the only place it is read.

Example:

{
  type: 'string',
  regexp: /\d+/,
  transformAfter: value => Number(value)
}

Reporting failures from a hook

Throw a ValidatorError with a code and, optionally, an info:

validateAfter: async (value) => {
  if (await isTaken(value)) {
    throw new ValidatorError('NICKNAME_TAKEN', {info: {nickname: value}});
  }
}

A hook knows its code and info but not where in the document it sits, so path and data are filled in by the validator.

Anything else a hook throws is treated as a bug rather than a validation failure: it is not converted into a ValidatorError and it rejects the promise instead of appearing in the result. A TypeError from a typo in a hook therefore surfaces as itself.

validate(data, {context = {}, maxDelayMsecs = validatorMaxDelayMsecs} = {})

Performs validation. Resolves with {ok: true, value} on success, or {ok: false, error} where error is a ValidatorError.

  • data -- data to be validated

Possible options are:

  • context -- a context passed to custom validation functions (an empty object by default)
  • maxDelayMsecs -- if an uninterrupted stretch of validation takes longer than this many milliseconds, the next portion is deferred to a later event loop turn. Infinity disables interruptions entirely. See Interruptions.

parse(data, {context = {}, maxDelayMsecs = validatorMaxDelayMsecs} = {})

Same as validate, but resolves with the validated value directly and rejects with a ValidatorError instead of returning it.

ValidatorErrorCode

Constant containing the error codes:

{
    BooleanNotInValues: 'BOOLEAN_NOT_IN_VALUES',
    CustomError: 'CUSTOM_ERROR',
    DuplicateItems: 'DUPLICATE_ITEMS',
    ExtraFields: 'EXTRA_FIELDS',
    InvalidArrayLength: 'INVALID_ARRAY_LENGTH',
    InvalidNumber: 'INVALID_NUMBER',
    InvalidString: 'INVALID_STRING',
    InvalidStringLength: 'INVALID_STRING_LENGTH',
    MissingField: 'MISSING_FIELD',
    NumberNotInteger: 'NUMBER_NOT_INTEGER',
    NumberNotInValues: 'NUMBER_NOT_IN_VALUES',
    NoMatchingVariant: 'NO_MATCHING_VARIANT',
    StringNotInValues: 'STRING_NOT_IN_VALUES',
    TypeMismatch: 'TYPE_MISMATCH',
    UnallowedNull: 'UNALLOWED_NULL'
}

Types

There are several builtin types.

object

A JavaScript object.

Example:

{
  type: 'object',
  fields: {
    // nested fields
  }
}

Data keys that are not declared in fields are treated as extra fields and handled according to extraStrategy (error by default, or exclude / include). Detection is based on own properties (Object.hasOwn), so a data key named after an inherited object member (e.g. toString, valueOf, constructor) is also reported as an extra field rather than being silently ignored.

array

A JavaScript array.

Example:

{
  type: 'array',
  element: {
    // element definition
  }
}

string

A string.

Example:

{
  type: 'string',
  minLength: [M], // optional, minimum string length
  maxLength: [N], // optional, maximum string length
  length: [K], // optional, exact string length
  regexp: [R] // optional, a regular expression to match the string against
}

number

A number.

Example:

{
  type: 'number',
  minValue: [M], // optional, minimum value
  maxValue: [N], // optional, maximum value
  isInteger: [true|false] // optional, requires the value to be a safe integer (Number.isSafeInteger)
}

NaN is not accepted as a valid number and fails with a TypeMismatch error. When isInteger is true, the value must be a safe integer: Infinity, -Infinity, NaN and integers outside the Number.isSafeInteger range (magnitude greater than 2^53 − 1, which cannot be represented exactly) are rejected with a NumberNotInteger error.

boolean

A boolean.

Example:

{
  type: 'boolean'
}

variant

Used to validate against multiple templates. The result of first successful validation is used.

Example:

{
  type: 'variant',
  variants: [{
    type: 'string',
    regexp: /\d+/,
    transformAfter: value => Number(value)
  }, {
    type: 'number'
  }]
}

If hint function is provided to template variants, raw data is checked against that function. Templates with hint returning false-ish values are excluded from validation. Templates are used in the same order they are specified. This may be used to optimize large template validation. Example:

[{
  type: 'object',
  hint: rawValue => rawValue.kind === 'KIND_A',
  fields: {
    kind: {
      type: 'string',
      values: ['KIND_A']
    }
    // Dozens of fields for kind A
  }
}, {
  type: 'object',
  hint: rawValue => rawValue.kind === 'KIND_B',
  fields: {
    kind: {
      type: 'string',
      values: ['KIND_B']
    }
    // Dozens of fields for kind B, different from the ones for kind A
  }
}]

If no variant matches the data, a NoMatchingVariant error occurs.

Advanced

isNullable

Any type can be allowed to be null.

Example:

{
  type: 'string',
  isNullable: true
}

Object prototype (class) as "type"

Example:

class CustomClass {
  constructor() {
    //
  }
}

{
  type: CustomClass
}

The value is checked with instanceof and passed through as it is, so an instance keeps its identity and its methods. Register a custom type to validate its contents.

Custom types

A custom type is registered on the validator and named by a template's type:

const validator = createValidator({type: 'isoDate'}, {
  customTypes: [{
    basicType: 'string',                                       // what the value must be
    match: (value) => !Number.isNaN(Date.parse(value)),        // whether this type applies
    type: 'isoDate',
    validate: (value) => new Date(value)                       // what the value becomes
  }]
});

basicType is what the value is held against before the custom type is given it — a basic type or a class. A custom type registered over a class is what makes {type: SomeClass} resolve to it, so a class-typed template and a named one reach the same code.

A custom type's validate replaces the body of the validation: it is handed the value and decides everything about it. None of the built-in options of its basicType is read, and a template naming a custom type may therefore not carry one. Where the built-in validation is wanted first, write it as the type it refines and chain into the custom type with next:

{
  type: 'array',
  element: {type: 'string'},
  next: {type: 'nonEmptyList'}   // gets the already validated array
}

Interruptions

Validating a large document is a long stretch of work with no I/O in it. Left alone it would hold the thread for its whole duration — starving incoming requests on the server, and blocking rendering and input in the browser. So the validator interrupts itself: whenever an uninterrupted stretch exceeds maxDelayMsecs (1 by default), it hands control back and resumes on a later turn of the event loop.

async/await does not provide this by itself. Awaiting yields to the microtask queue, and that queue is drained completely before the event loop advances to its next phase — a traversal made entirely of await blocks I/O exactly as thoroughly as a synchronous loop would. An interruption therefore has to be a macrotask, and the mechanism is picked by feature detection:

  • setImmediate where it exists (Node);
  • otherwise a MessageChannel round trip (the browser) — unlike setTimeout(fn, 0), it is not subject to the 4ms clamp the HTML specification imposes on nested timeouts, and every interruption here is scheduled from inside the previous one;
  • setTimeout as a last resort.

Set maxDelayMsecs: Infinity to turn interruptions off, for instance when validating small payloads in a hot path. Nothing then reads the clock either.

TypeScript

A validator carries the type its template produces, so a successful result needs no annotation and no cast:

const UserValidator = createValidator({
  type: 'object',
  fields: {
    nickname: {type: 'string', minLength: 4},
    role: {type: 'string', values: ['admin', 'user']},
    kittens: {type: 'number', missingStrategy: 'default', defaultValue: 0},
    bio: {type: 'string', isNullable: true, missingStrategy: 'ignore'},
    tags: {type: 'array', element: {type: 'string'}}
  }
});

const result = await UserValidator.validate(payload);

if (!result.ok) {
  return reply.code(400).send({code: result.error.code, path: result.error.path});
}

result.value;
//     ^? {
//          nickname: string;
//          role: 'admin' | 'user';
//          kittens: number;
//          bio?: string | null;
//          tags: string[];
//        }

No as const is required — the template literal is captured through a const type parameter. Templates may be written with the exported enums (BasicType.String) or with plain strings ('string'); both infer identically.

What the inference follows:

| in the template | in the result | |---|---| | values: [...] | a literal union rather than string / number / boolean | | isNullable: true | T \| null | | missingStrategy: 'ignore' | the field becomes optional — the validator really does omit it | | missingStrategy: 'default' | union with the type of defaultValue | | type: 'array' | Infer<element>[] | | type: 'variant' | a union, discriminated when the variants share a values-constrained field | | type: SomeClass | InstanceType<SomeClass> | | extraStrategy: 'include' | adds [key: string]: unknown | | transformAfter | whatever it returns, with a promise unwrapped | | next | the type of the chained template | | a custom type by name | whatever that type's validate returns |

Infer is exported for the cases where the type is needed on its own:

import type {Infer} from '@ololoepepe/validator';

type User = Infer<typeof UserValidator.template>;

Recursive templates

A template that refers to itself would make the type checker recurse forever, so the reference is deferred and annotated once with lazy:

interface Comment {
  text: string;
  replies?: Comment[];
}

const commentTemplate: TemplateShape = {
  type: 'object',
  fields: {
    text: {type: 'string', minLength: 1},
    replies: {
      type: 'array',
      missingStrategy: 'ignore',
      element: lazy<Comment>(() => commentTemplate)
    }
  }
};

The thunk is resolved when validation first reaches that node, and the resolved template is validated and cached like any other, so the cycle terminates.

How a wrong option is reported

TemplateShape is a union discriminated by typeStringTemplateShape, NumberTemplateShape and one member per type, each declaring the options that type reads. createValidator takes the template as CheckedTemplate<T> & T, which resolves that member from the template's own type and gives every option the type does not have the type never:

createValidator({type: 'number', maxLength: 3});
//                               ~~~~~~~~~ Type 'number' is not assignable to type 'never'

The constraint alone cannot do this. TypeScript checks an inferred type argument against its constraint by plain assignability, and assignability ignores extra properties, so a misspelled option satisfies T extends TemplateShape unopposed however precisely the shape is written — which is why the check is a separate type in the parameter rather than a stricter constraint.

An option the type cannot do without is demanded there as well, a mapped type otherwise walking the options that are present rather than the ones that should be:

createValidator({type: 'array'});
// Property 'element' is missing in type '{ type: "array"; }' but required in type
// 'RequiredOptions<ArrayTemplateShape>'

A template written behind an annotation (const t: TemplateShape = {...}) is checked by the excess property check instead, which reports the same mistake with a suggestion:

const t: TemplateShape = {type: 'string', maxLenght: 4};
// Object literal may only specify known properties, but 'maxLenght' does not exist in type
// 'CustomTemplateShape | StringTemplateShape'. Did you mean to write 'maxLength'?

Neither reaches a template assembled at runtime, and neither exists in JavaScript, so validateTemplate makes the same check when the validator is created — see The option set is closed.

What is not inferred

  • The input of a hook. transformAfter: (value: string) => Number(value) needs the parameter annotated; only the result is inferred. TypeScript cannot use a partially inferred template to contextually type another property of the same literal.
  • A custom type reached through a class. {type: SomeClass} infers InstanceType<SomeClass>. When a custom type registered under that class returns something else from validate, refer to it by its string name instead.
  • A template stored with a widening annotation. const t: TemplateShape = {...} erases the literal types; leave the annotation off, or use lazy where one is needed to break a cycle.