# Ascertain

Compiled schema-and-constraint validation for JavaScript-native runtime values.

Write schemas as native JavaScript values, compile them once, and validate at AJV-class or better speed with zero dependencies and detailed pathful errors.

[![Coverage Status][codecov-image]][codecov-url]
[![Build Status][github-image]][github-url]
[![NPM version][npm-image]][npm-url]
[![Downloads][downloads-image]][npm-url]

## Table of Contents

- [Features](#features)
- [Install](#install)
- [Quick Start](#quick-start)
- [Performance](#performance)
- [Schema Reference](#schema-reference)
- [Type Casting](#type-casting)
- [Patterns](#patterns)
- [Compile Options](#compile-options)
- [Standard Schema](#standard-schema)
- [Complete Example](#complete-example)
- [Documentation](https://3axap4ehko.github.io/ascertain/) <!--API_TOC-->
- [License](#license)

## Features

- **JavaScript-native schemas** - Use constructors, literals, regexes, arrays, and object shapes directly
- **Compiled validators** - `compile()` emits specialized JS validators instead of interpreting the schema on every call
- **Fast first-error and all-errors modes** - Stay fast on valid data and on bad input
- **Zero dependencies** - Small footprint for Node.js and browsers
- **Type-safe** - Full TypeScript support with type inference
- **Flexible schemas** - AND, OR, optional, tuple, discriminated, and custom check operators
- **Object validation** - Validate keys/values with `$keys`, `$values`, `$strict`
- **Type casting** - Built-in parsers for numbers, dates, JSON, base64, bytes, and durations
- **Partial validation** - `createValidator` validates subsets with type narrowing
- **Standard Schema v1** - Interoperable with tRPC, TanStack Form, and other ecosystem tools

## Install

```bash
npm install ascertain
```

## Quick Start

```typescript
import { compile, discriminated, optional, or } from 'ascertain';

const validateUser = compile({
  id: Number,
  role: or('admin', 'user', 'guest'),
  score: optional(Number),
  notifications: [
    discriminated(
      [
        { type: 'email', address: String },
        { type: 'sms', phone: String },
        { type: 'push', token: String },
      ],
      'type',
    ),
  ],
});

if (!validateUser(userData)) {
  console.error(validateUser.issues);
}
```

Use `ascertain(schema, data)` when you want the convenience wrapper that compiles and throws immediately on failure.

## Performance

`compile()` is the center of the library. It turns a schema written as native JS values into a specialized validator function that you can reuse in hot paths.

```typescript
import { compile } from 'ascertain';

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

// Validate many (no recompilation)
validateUser(user1);
validateUser(user2);
```

| When to use | Function | Why |
|-------------|----------|-----|
| Repeated validation (API handlers, loops, message boundaries) | `compile()` | Compile once, reuse the generated validator |
| One-off validation | `ascertain()` | Convenience wrapper around `compile()` that throws on failure |

### Benchmark

Maintainer-run benchmark suites live in `src/__bench__/benchmark.ts` and `src/__bench__/benchmark-complex.ts`. On the current checkout, Ascertain is consistently faster than AJV in this suite and dramatically faster than Zod on invalid all-errors workloads.

Comparable all-errors results from the current run:

| Workload | Ascertain | AJV | Zod |
|----------|-----------|-----|-----|
| Simple valid | **350.9M** ops/s | 88.7M ops/s | 57.9M ops/s |
| Simple invalid | **38.2M** ops/s | 28.2M ops/s | 75.7K ops/s |
| Complex valid | **47.8M** ops/s | 38.1M ops/s | 6.1M ops/s |
| Complex invalid | **16.9M** ops/s | 11.7M ops/s | 44.0K ops/s |

First-error mode is the default and pushes invalid-path throughput higher still. In the same run, Ascertain reached 91.1M ops/s on simple invalid data and 83.6M ops/s on complex invalid data.

Benchmark sources: [`src/__bench__/benchmark.ts`](https://github.com/3axap4eHko/ascertain/blob/master/src/__bench__/benchmark.ts), [`src/__bench__/benchmark-complex.ts`](https://github.com/3axap4eHko/ascertain/blob/master/src/__bench__/benchmark-complex.ts), [`src/__bench__/self.ts`](https://github.com/3axap4eHko/ascertain/blob/master/src/__bench__/self.ts)

Run it locally:

```bash
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

| Schema | Validates | Example |
|--------|-----------|---------|
| `String`, `Number`, `Boolean` | Type check | `{ age: Number }` |
| `Date`, `Array`, `Object` | Instance check | `{ created: Date }` |
| `Function` | Any callable | `{ handler: Function }` |
| Primitives | Exact value | `{ status: 'active' }` |
| RegExp | Pattern match | `{ email: /^.+@.+$/ }` |
| `[Schema]` | Array of type | `{ tags: [String] }` |
| `{ key: Schema }` | Object shape | `{ user: { name: String } }` |
| `or(a, b, ...)` | Any match | `or(String, Number)` |
| `and(a, b, ...)` | All match | `and(Date, { toJSON: Function })` |
| `optional(s)` | Nullable | `optional(String)` |
| `tuple(a, b)` | Fixed array | `tuple(Number, Number)` |
| `discriminated(schemas, key)` | Tagged union | `discriminated([{ type: 'a' }, { type: 'b' }], 'type')` |

### Special Symbols

```typescript
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):

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
// 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:

```typescript
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:

```typescript
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](https://standardschema.dev/) compliance, enabling interoperability with tRPC, TanStack Form, and other ecosystem libraries:

```typescript
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

```typescript
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_REFERENCE-->

## License

[MIT](http://opensource.org/licenses/MIT) - Ivan Zakharchanka

[npm-url]: https://www.npmjs.com/package/ascertain
[downloads-image]: https://img.shields.io/npm/dw/ascertain.svg?maxAge=43200
[npm-image]: https://img.shields.io/npm/v/ascertain.svg?maxAge=43200
[github-url]: https://github.com/3axap4eHko/ascertain/actions/workflows/cicd.yml
[github-image]: https://github.com/3axap4eHko/ascertain/actions/workflows/cicd.yml/badge.svg
[codecov-url]: https://codecov.io/gh/3axap4eHko/ascertain
[codecov-image]: https://img.shields.io/codecov/c/github/3axap4eHko/ascertain/master.svg?maxAge=43200
