Ascertain
Home
GitHub
Home
GitHub
  • Ascertain

Ascertain

Zero-dependency, high-performance schema validator for Node.js and browsers.

Coverage StatusBuild StatusNPM versionDownloadsSnyk

Table of Contents

  • Features
  • Install
  • Quick Start
  • Performance
  • Schema Reference
  • Type Casting
  • Patterns
  • Compile Options
  • Standard Schema
  • Complete Example
  • API
    • $keys
    • $values
    • $strict
    • or(...schemas: Schema[]): OrShape
    • and(...schemas: Schema[]): AndShape
    • optional(schema: Schema): OptionalShape
    • tuple(...schemas: Schema[]): TupleShape
    • discriminated(schemas: Schema[], key: string): DiscriminatedShape
    • check(fnOrOpts, message?: string): CheckShape
    • min(n: number, message?: string): CheckShape
    • max(n: number, message?: string): CheckShape
    • integer(message?: string): CheckShape
    • minLength(n: number, message?: string): CheckShape
    • maxLength(n: number, message?: string): CheckShape
    • gt(n: number, message?: string): CheckShape
    • lt(n: number, message?: string): CheckShape
    • multipleOf(n: number, message?: string): CheckShape
    • uniqueItems(message?: string): CheckShape
    • oneOf(values: string | number[] | T, message?: string): CheckShape
    • fromBase64
    • asError(message: string)
    • as
    • format
    • compile(schema: Schema, options?: CompileOptions): Validator
    • ascertain(schema: Schema, data: T)
    • createValidator(config: C)
    • standardSchema(schema: Schema): StandardSchemaFn
  • License

Features

  • Zero dependencies - Minimal footprint, no external dependencies
  • High performance - Compiles schemas to optimized JS functions (~6x faster than dynamic validation)
  • Type-safe - Full TypeScript support with type inference
  • Flexible schemas - AND, OR, optional, tuple, discriminated operators
  • Type casting - Built-in parsers for numbers (hex, octal, binary), dates, JSON, base64
  • Object validation - Validate keys/values with $keys, $values, $strict
  • Partial validation - createValidator validates subsets with type narrowing
  • Detailed errors - Clear error messages with paths for debugging
  • Standard Schema v1 - Interoperable with tRPC, TanStack Form, and other ecosystem tools

Install

npm install ascertain

Quick Start

import { ascertain, or, optional } from 'ascertain';

ascertain({
  name: String,
  age: Number,
  role: or('admin', 'user'),
  email: optional(String),
}, userData);

Performance

Ascertain compiles schemas into optimized JavaScript functions. Compiled validators run ~6x faster than dynamic validation.

import { compile } from 'ascertain';

// Compile once
const validateUser = compile(userSchema);

// Validate many (no recompilation)
validateUser(user1);
validateUser(user2);
When to useFunctionSpeed
Repeated validation (API handlers, loops)compile()Fastest
One-off validationascertain()Convenient

Benchmark

LibraryModeValid (ops/s)Invalid (ops/s)
Ascertainfirst-error322M84M
Ascertainall-errors325M36M
AJVfirst-error92M65M
AJVall-errors92M30M
Zodall-errors62M72K

Benchmark source: src/__bench__/benchmark.ts

Run it locally:

pnpm build
pnpm bench

Notes:

  • Maintainer-run benchmark, not an independent study
  • Measures valid and invalid paths for compiled validators
  • Compares the current checkout, published ascertain, AJV, and Zod
  • Results vary by CPU, Node.js version, and workload, so treat the numbers as directional

Schema Reference

SchemaValidatesExample
String, Number, BooleanType check{ age: Number }
Date, Array, ObjectInstance check{ created: Date }
FunctionAny callable{ handler: Function }
PrimitivesExact value{ status: 'active' }
RegExpPattern match{ email: /^.+@.+$/ }
[Schema]Array of type{ tags: [String] }
{ key: Schema }Object shape{ user: { name: String } }
or(a, b, ...)Any matchor(String, Number)
and(a, b, ...)All matchand(Date, { toJSON: Function })
optional(s)Nullableoptional(String)
tuple(a, b)Fixed arraytuple(Number, Number)
discriminated(schemas, key)Tagged uniondiscriminated([{ type: 'a' }, { type: 'b' }], 'type')

Special Symbols

import { $keys, $values, $strict } from 'ascertain';

const schema = {
  [$keys]: /^[a-z]+$/,   // Validate all keys
  [$values]: Number,      // Validate all values
  [$strict]: true,        // No extra properties
};

Type Casting

Parse strings into typed values (environment variables, query params):

import { as } from 'ascertain';

as.number('42')        // 42
as.number('3.14')      // 3.14
as.number('0xFF')      // 255 (hex)
as.number('0o77')      // 63 (octal)
as.number('0b1010')    // 10 (binary)
as.number('1e10')      // 10000000000

as.boolean('true')     // true
as.boolean('1')        // true

as.time('500ms')       // 500
as.time('30s')         // 30000
as.time('5m')          // 300000
as.time('2h')          // 7200000
as.time('1d')          // 86400000

as.date('2024-12-31')  // Date object
as.array('a,b,c', ',') // ['a', 'b', 'c']
as.json('{"x":1}')     // { x: 1 }
as.base64('dGVzdA==')  // 'test'

Invalid values return TypeError for deferred validation:

const config = {
  port: as.number(process.env.PORT),  // TypeError if invalid
  host: as.string(process.env.HOST),
};

// Errors surface with clear paths
ascertain({ port: Number, host: String }, config);
// → TypeError: "Invalid value undefined, expected a string"

Patterns

Batch Validation

Compile once, validate many:

const validateUser = compile(userSchema);

const results = users.map((user, i) => {
  if (validateUser(user)) {
    return { index: i, valid: true };
  }
  return { index: i, valid: false, error: validateUser.issues[0].message };
});

Discriminated Unions

Use discriminated() for efficient tagged union validation. Instead of trying each variant like or(), it checks the discriminant field first and only validates the matching variant:

import { compile, discriminated } from 'ascertain';

const messageSchema = discriminated([
  { type: 'email', address: String },
  { type: 'sms', phone: String },
  { type: 'push', token: String },
], 'type');

const validate = compile(messageSchema);

validate({ type: 'email', address: 'user@example.com' }); // true
validate({ type: 'sms', phone: '123456' });                // true
validate({ type: 'push', token: 123 });                    // false
validate({ type: 'unknown' });                             // false

Discriminant values must be string, number, or boolean literals.

Conditional Rules

Use or() and and() for complex conditions:

const schema = {
  type: or('email', 'sms'),
  // Conditional: email requires address, sms requires phone
  contact: or(
    and({ type: 'email' }, { address: String }),
    and({ type: 'sms' }, { phone: String }),
  ),
};

Schema Composition

Build schemas from reusable parts:

const addressSchema = {
  street: String,
  city: String,
  zip: /^\d{5}$/,
};

const personSchema = {
  name: String,
  address: addressSchema,
};

const companySchema = {
  name: String,
  headquarters: addressSchema,
  employees: [personSchema],
};

Versioned Schemas

Version schemas as modules:

// schemas/user.v1.ts
export const userSchemaV1 = { name: String, email: String };

// schemas/user.v2.ts
export const userSchemaV2 = {
  ...userSchemaV1,
  phone: optional(String),
  createdAt: Date,
};

// api/handler.ts
import { userSchemaV2 } from './schemas/user.v2';
const validate = compile(userSchemaV2);

Config Validation

Validate only what each module needs:

import { createValidator, as } from 'ascertain';

const config = {
  app: { name: as.string(process.env.APP_NAME), port: as.number(process.env.PORT) },
  db: { host: as.string(process.env.DB_HOST), pool: as.number(process.env.DB_POOL) },
  cache: { ttl: as.time(process.env.CACHE_TTL) },
};

const validate = createValidator(config);

// Each module validates only what it needs
const { db } = validate({
  db: { host: String, pool: Number },
});

db.host;   // string - validated and typed
db.pool;   // number - validated and typed
// db.xxx  // TypeScript error - property doesn't exist

// cache not validated = not accessible
// cache.ttl  // TypeScript error - cache not in returned type

Compile Options

By default compile() stops at the first validation error (fastest for invalid data). Pass { allErrors: true } to collect all errors:

import { compile } from 'ascertain';

const schema = { name: String, age: Number, active: Boolean };

// First-error mode (default) - stops at first failure
const validate = compile(schema);
if (!validate({ name: 123, age: 'bad', active: 'no' })) {
  console.log(validate.issues.length); // 1
  console.log(validate.issues[0].path); // ['name']
}

// All-errors mode - collects every failure
const validateAll = compile(schema, { allErrors: true });
if (!validateAll({ name: 123, age: 'bad', active: 'no' })) {
  console.log(validateAll.issues.length); // 3
}

Standard Schema

Wrap a schema for Standard Schema v1 compliance, enabling interoperability with tRPC, TanStack Form, and other ecosystem libraries:

import { standardSchema, or, optional } from 'ascertain';

const userValidator = standardSchema({
  name: String,
  age: Number,
  role: or('admin', 'user'),
  email: optional(String),
});

// Use as regular validator (throws on error)
userValidator({ name: 'Alice', age: 30, role: 'admin' });

// Use Standard Schema interface (returns result object)
const result = userValidator['~standard'].validate(unknownData);
if (result.issues) {
  console.log(result.issues);
} else {
  console.log(result.value);
}

Complete Example

import { compile, or, optional, and, tuple, $keys, $values, $strict, as } from 'ascertain';

const schema = {
  id: Number,
  name: String,
  email: /^[^@]+@[^@]+$/,
  status: or('active', 'inactive', 'pending'),
  role: optional(or('admin', 'user')),

  profile: {
    bio: optional(String),
    avatar: optional(String),
  },

  settings: {
    [$keys]: /^[a-z_]+$/,
    [$values]: or(String, Number, Boolean),
    [$strict]: true,
  },

  coordinates: optional(tuple(Number, Number)),
  createdAt: and(Date, { toISOString: Function }),

  retries: as.number(process.env.MAX_RETRIES),
  timeout: as.time(process.env.TIMEOUT),
};

const validate = compile(schema);
if (!validate(data)) {
  console.error(validate.issues);
}

API

$keys

Symbol for validating object keys against a schema.

$values

Symbol for validating object values against a schema.

$strict

Symbol for enforcing strict object validation (no extra properties allowed).

or(...schemas: Schema[]): OrShape

Operator for validating data against any of the provided schemas (logical OR).

and(...schemas: Schema[]): AndShape

Operator for validating data against all provided schemas (logical AND).

optional(schema: Schema): OptionalShape

Operator for making a schema optional (nullable).

tuple(...schemas: Schema[]): TupleShape

Operator for validating data against a fixed-length tuple of schemas.

discriminated(schemas: Schema[], key: string): DiscriminatedShape

Operator for validating data against a discriminated union.

Optimizes validation by checking the discriminant field first and only validating the matching variant. More efficient than or() for unions where each variant has a common field with a unique literal value.

  • @param schemas - Array of object schemas, each with a discriminant field containing a literal value.

  • @param key - The name of the discriminant field present in all variants.

  • @example

const messageSchema = discriminated([
  { type: 'email', address: String },
  { type: 'sms', phone: String },
  { type: 'push', token: String },
], 'type');

check(fnOrOpts, message?: string): CheckShape

Creates a custom validation check. Accepts a predicate function or an object with a compile method for inlined checks.

  • @param fnOrOpts - A predicate function (value) => boolean or an object with a compile method for code-generating checks.
  • @param message - Optional custom error message.

min(n: number, message?: string): CheckShape

Validates that a numeric value is greater than or equal to n.

  • @param n - The minimum allowed value (inclusive).
  • @param message - Optional custom error message.

max(n: number, message?: string): CheckShape

Validates that a numeric value is less than or equal to n.

  • @param n - The maximum allowed value (inclusive).
  • @param message - Optional custom error message.

integer(message?: string): CheckShape

Validates that a value is an integer.

  • @param message - Optional custom error message.

minLength(n: number, message?: string): CheckShape

Validates that a value's length is greater than or equal to n.

  • @param n - The minimum allowed length (inclusive).
  • @param message - Optional custom error message.

maxLength(n: number, message?: string): CheckShape

Validates that a value's length is less than or equal to n.

  • @param n - The maximum allowed length (inclusive).
  • @param message - Optional custom error message.

gt(n: number, message?: string): CheckShape

Validates that a numeric value is strictly greater than n.

  • @param n - The exclusive lower bound.
  • @param message - Optional custom error message.

lt(n: number, message?: string): CheckShape

Validates that a numeric value is strictly less than n.

  • @param n - The exclusive upper bound.
  • @param message - Optional custom error message.

multipleOf(n: number, message?: string): CheckShape

Validates that a numeric value is a multiple of n.

  • @param n - The divisor to check against.
  • @param message - Optional custom error message.

uniqueItems(message?: string): CheckShape

Validates that an array contains only unique items.

  • @param message - Optional custom error message.

oneOf(values: string | number[] | T, message?: string): CheckShape

Validates that a value is one of the allowed values. Accepts an array or an enum-like object.

  • @param values - Array of allowed values or an enum-like object.
  • @param message - Optional custom error message.

fromBase64

Decodes a base64-encoded string to UTF-8.

Uses Buffer in Node.js environments and atob in browsers.

  • @param value - The base64-encoded string to decode.
  • @returns The decoded UTF-8 string.

asError(message: string)

Creates a TypeError with the given message, typed as T for deferred error handling.

Used by as.* conversion utilities to return errors that can be caught during schema validation rather than throwing immediately.

  • @template T - The expected return type (for type compatibility with conversion functions).
  • @param message - The error message.
  • @returns A TypeError instance typed as T.

as

Type casting utilities for parsing strings into typed values. Useful for environment variables, query parameters, and other string inputs. Returns a TypeError for invalid values, enabling deferred validation with ascertain().

format

String format validators for common patterns (RFC 3339 date-time, email, URI, UUID, etc.). Each method returns a CheckShape that can be composed with and() for schema validation.

compile(schema: Schema, options?: CompileOptions): Validator

Compiles a schema into a high-performance validation function.

By default uses first-error mode which stops at the first validation failure and returns immediately. This provides optimal performance for invalid data.

Set allErrors: true to collect all validation errors (slower but more informative).

  • @template T - The type of data the schema validates.

  • @param schema - The schema to compile.

  • @param options - Optional configuration (allErrors: boolean).

  • @returns A validator function that returns boolean. Access .issues for error details.

  • @example

import { compile, optional, or } from 'ascertain';

const userSchema = {
  name: String,
  age: Number,
  email: optional(String),
  role: or('admin', 'user', 'guest')
};

// First-error mode (default) - fastest for invalid data
const validate = compile(userSchema);

// All-errors mode - collects all validation issues
const validateAll = compile(userSchema, { allErrors: true });

// Valid data
if (validate({ name: 'John', age: 30, role: 'user' })) {
  console.log('Valid!');
}

// Invalid data - check .issues for details
if (!validate({ name: 123, age: 'thirty' })) {
  console.log(validate.issues); // Array with first validation issue
}

ascertain(schema: Schema, data: T)

Asserts that data conforms to a given schema.

This function is a convenient wrapper around compile. It compiles the schema and immediately validates the provided data against it.

  • @template T - The type of data the schema validates.

  • @param schema - The schema to validate against.

  • @param data - The data to validate.

  • @throws {TypeError} If the data does not conform to the schema.

  • @example

import { ascertain, optional, and, or } from 'ascertain';

const userSchema = {
  name: String,
  age: Number,
  email: optional(String),
  active: Boolean
};

const userData = {
  name: 'Alice',
  age: 25,
  email: 'alice-@example.com',
  active: true
};

// Validate data - throws if invalid, otherwise continues silently
ascertain(userSchema, userData);
console.log('User data is valid!');

// Example with invalid data
try {
  ascertain(userSchema, {
    name: 'Bob',
    age: 'twenty-five', // Invalid: should be number
    active: true
  });
} catch (error) {
  console.error('Validation failed:', error.message);
}
  • @example
// Array validation
const numbersSchema = [Number];
const numbers = [1, 2, 3, 4, 5];

ascertain(numbersSchema, numbers);

// Tuple validation
const coordinateSchema = tuple(Number, Number);
const point = [10, 20];

ascertain(coordinateSchema, point);

createValidator(config: C)

Creates a typed validator function for a config object.

Returns a function that validates a schema against the config and returns the same config reference with a narrowed type containing only the validated fields.

  • @template C - The type of the config object.

  • @param config - The config object to validate against.

  • @returns A validator function that takes a schema and returns the typed config subset.

  • @example

import { createValidator, as } from 'ascertain';

const config = {
  app: { name: as.string(process.env.APP_NAME) },
  kafka: { brokers: as.array(process.env.BROKERS, ',') },
  redis: { host: as.string(process.env.REDIS_HOST) },
};

const validate = createValidator(config);

// Consumer only validates what it needs
const { app, kafka } = validate({
  app: { name: String },
  kafka: { brokers: [String] },
});

// app.name is typed as string
// kafka.brokers is typed as string[]
// redis is not accessible - TypeScript error

standardSchema(schema: Schema): StandardSchemaFn

Wraps a schema for Standard Schema v1 compliance. The returned function throws on invalid data and exposes a ~standard interface for interoperability with tRPC, TanStack Form, and other ecosystem tools.

  • @param schema - The schema to wrap.
  • @returns A validator function with a ~standard property conforming to Standard Schema v1.

License

MIT - Ivan Zakharchanka

Last Updated: 3/17/26, 3:58 PM
Contributors: Ivan Zakharchanka, 3axap4eHko