UNPKG

ascertain

Version:

0-Deps, simple, fast, for browser and node js object schema validator

400 lines (303 loc) 11.1 kB
# Ascertain Zero-dependency, high-performance schema validator for Node.js and browsers. [![Coverage Status][codecov-image]][codecov-url] [![Build Status][github-image]][github-url] [![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url] [![Snyk][snyk-image]][snyk-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 - **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 ```bash npm install ascertain ``` ## Quick Start ```typescript 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. ```typescript import { compile } from 'ascertain'; // Compile once const validateUser = compile(userSchema); // Validate many (no recompilation) validateUser(user1); validateUser(user2); ``` | When to use | Function | Speed | |-------------|----------|-------| | Repeated validation (API handlers, loops) | `compile()` | Fastest | | One-off validation | `ascertain()` | Convenient | ### Benchmark | Library | Mode | Valid (ops/s) | Invalid (ops/s) | |---------|------|---------------|-----------------| | **Ascertain** | first-error | 322M | 84M | | **Ascertain** | all-errors | 325M | 36M | | AJV | first-error | 92M | 65M | | AJV | all-errors | 92M | 30M | | Zod | all-errors | 62M | 72K | Benchmark source: [`src/__bench__/benchmark.ts`](https://github.com/3axap4eHko/ascertain/blob/master/src/__bench__/benchmark.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 [snyk-url]: https://snyk.io/test/npm/ascertain/latest [snyk-image]: https://img.shields.io/snyk/vulnerabilities/github/3axap4eHko/ascertain.svg?maxAge=43200