ascertain 2.1.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -114
- package/build/index.cjs +616 -183
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +215 -186
- package/build/index.js +577 -183
- package/build/index.js.map +1 -1
- package/package.json +15 -13
- package/src/index.ts +777 -380
package/README.md
CHANGED
|
@@ -12,135 +12,237 @@ Zero-dependency, high-performance schema validator for Node.js and browsers.
|
|
|
12
12
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
|
-
- **Zero dependencies
|
|
16
|
-
- **High performance
|
|
17
|
-
- **Type-safe
|
|
18
|
-
- **Flexible schemas
|
|
19
|
-
- **Type casting
|
|
20
|
-
- **Object validation
|
|
21
|
-
- **
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
- **Zero dependencies** - Minimal footprint, no external dependencies
|
|
16
|
+
- **High performance** - Compiles schemas to optimized JS functions (~6x faster than dynamic validation)
|
|
17
|
+
- **Type-safe** - Full TypeScript support with type inference
|
|
18
|
+
- **Flexible schemas** - AND, OR, optional, tuple, discriminated operators
|
|
19
|
+
- **Type casting** - Built-in parsers for numbers (hex, octal, binary), dates, JSON, base64
|
|
20
|
+
- **Object validation** - Validate keys/values with `$keys`, `$values`, `$strict`
|
|
21
|
+
- **Partial validation** - `createValidator` validates subsets with type narrowing
|
|
22
|
+
- **Detailed errors** - Clear error messages with paths for debugging
|
|
23
|
+
- **Standard Schema v1** - Interoperable with tRPC, TanStack Form, and other ecosystem tools
|
|
24
|
+
|
|
25
|
+
## Install
|
|
24
26
|
|
|
25
27
|
```bash
|
|
26
28
|
npm install ascertain
|
|
27
|
-
# or
|
|
28
|
-
pnpm add ascertain
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## Quick Start
|
|
32
32
|
|
|
33
33
|
```typescript
|
|
34
|
-
import { ascertain,
|
|
34
|
+
import { ascertain, or, optional } from 'ascertain';
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
const userSchema = {
|
|
36
|
+
ascertain({
|
|
38
37
|
name: String,
|
|
39
38
|
age: Number,
|
|
39
|
+
role: or('admin', 'user'),
|
|
40
40
|
email: optional(String),
|
|
41
|
-
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// Validate data (throws on invalid)
|
|
45
|
-
ascertain(userSchema, userData, 'User');
|
|
41
|
+
}, userData);
|
|
46
42
|
```
|
|
47
43
|
|
|
48
44
|
## Performance
|
|
49
45
|
|
|
50
|
-
Ascertain compiles schemas into optimized JavaScript functions
|
|
51
|
-
|
|
52
|
-
**For best performance, compile schemas once and reuse the validator:**
|
|
46
|
+
Ascertain compiles schemas into optimized JavaScript functions. Compiled validators run **~6x faster** than dynamic validation.
|
|
53
47
|
|
|
54
48
|
```typescript
|
|
55
49
|
import { compile } from 'ascertain';
|
|
56
50
|
|
|
57
|
-
// Compile once
|
|
58
|
-
const validateUser = compile(userSchema
|
|
51
|
+
// Compile once
|
|
52
|
+
const validateUser = compile(userSchema);
|
|
59
53
|
|
|
60
|
-
//
|
|
54
|
+
// Validate many (no recompilation)
|
|
61
55
|
validateUser(user1);
|
|
62
56
|
validateUser(user2);
|
|
63
57
|
```
|
|
64
58
|
|
|
65
|
-
|
|
59
|
+
| When to use | Function | Speed |
|
|
60
|
+
|-------------|----------|-------|
|
|
61
|
+
| Repeated validation (API handlers, loops) | `compile()` | Fastest |
|
|
62
|
+
| One-off validation | `ascertain()` | Convenient |
|
|
66
63
|
|
|
67
64
|
### Benchmark
|
|
68
65
|
|
|
69
|
-
| Library |
|
|
70
|
-
|
|
71
|
-
| **Ascertain** |
|
|
72
|
-
|
|
|
73
|
-
|
|
|
66
|
+
| Library | Mode | Valid (ops/s) | Invalid (ops/s) |
|
|
67
|
+
|---------|------|---------------|-----------------|
|
|
68
|
+
| **Ascertain** | first-error | 80M | 41M |
|
|
69
|
+
| **Ascertain** | all-errors | 80M | 25M |
|
|
70
|
+
| AJV | first-error | 52M | 35M |
|
|
71
|
+
| AJV | all-errors | 52M | 20M |
|
|
72
|
+
| Zod | all-errors | 34M | 77K |
|
|
73
|
+
|
|
74
|
+
## Schema Reference
|
|
75
|
+
|
|
76
|
+
| Schema | Validates | Example |
|
|
77
|
+
|--------|-----------|---------|
|
|
78
|
+
| `String`, `Number`, `Boolean` | Type check | `{ age: Number }` |
|
|
79
|
+
| `Date`, `Array`, `Object` | Instance check | `{ created: Date }` |
|
|
80
|
+
| `Function` | Any callable | `{ handler: Function }` |
|
|
81
|
+
| Primitives | Exact value | `{ status: 'active' }` |
|
|
82
|
+
| RegExp | Pattern match | `{ email: /^.+@.+$/ }` |
|
|
83
|
+
| `[Schema]` | Array of type | `{ tags: [String] }` |
|
|
84
|
+
| `{ key: Schema }` | Object shape | `{ user: { name: String } }` |
|
|
85
|
+
| `or(a, b, ...)` | Any match | `or(String, Number)` |
|
|
86
|
+
| `and(a, b, ...)` | All match | `and(Date, { toJSON: Function })` |
|
|
87
|
+
| `optional(s)` | Nullable | `optional(String)` |
|
|
88
|
+
| `tuple(a, b)` | Fixed array | `tuple(Number, Number)` |
|
|
89
|
+
| `discriminated(schemas, key)` | Tagged union | `discriminated([{ type: 'a' }, { type: 'b' }], 'type')` |
|
|
90
|
+
|
|
91
|
+
### Special Symbols
|
|
74
92
|
|
|
75
|
-
```
|
|
76
|
-
|
|
93
|
+
```typescript
|
|
94
|
+
import { $keys, $values, $strict } from 'ascertain';
|
|
95
|
+
|
|
96
|
+
const schema = {
|
|
97
|
+
[$keys]: /^[a-z]+$/, // Validate all keys
|
|
98
|
+
[$values]: Number, // Validate all values
|
|
99
|
+
[$strict]: true, // No extra properties
|
|
100
|
+
};
|
|
77
101
|
```
|
|
78
102
|
|
|
79
|
-
##
|
|
103
|
+
## Type Casting
|
|
80
104
|
|
|
81
|
-
|
|
82
|
-
|--------|-------------|---------|
|
|
83
|
-
| Primitives | Match exact values | `42`, `'active'`, `true`, `null` |
|
|
84
|
-
| Constructors | Validate by type | `String`, `Number`, `Boolean`, `Date` |
|
|
85
|
-
| Arrays | Validate array items | `[Number]` (array of numbers) |
|
|
86
|
-
| Objects | Validate properties | `{ name: String, age: Number }` |
|
|
87
|
-
| RegExp | Match string patterns | `/^[a-z]+$/` |
|
|
88
|
-
| `or()` | Match any schema | `or(String, Number)` |
|
|
89
|
-
| `and()` | Match all schemas | `and(Date, { toJSON: Function })` |
|
|
90
|
-
| `optional()` | Allow null/undefined | `optional(String)` |
|
|
91
|
-
| `tuple()` | Fixed-length arrays | `tuple(Number, Number)` |
|
|
105
|
+
Parse strings into typed values (environment variables, query params):
|
|
92
106
|
|
|
93
|
-
|
|
107
|
+
```typescript
|
|
108
|
+
import { as } from 'ascertain';
|
|
109
|
+
|
|
110
|
+
as.number('42') // 42
|
|
111
|
+
as.number('3.14') // 3.14
|
|
112
|
+
as.number('0xFF') // 255 (hex)
|
|
113
|
+
as.number('0o77') // 63 (octal)
|
|
114
|
+
as.number('0b1010') // 10 (binary)
|
|
115
|
+
as.number('1e10') // 10000000000
|
|
116
|
+
|
|
117
|
+
as.boolean('true') // true
|
|
118
|
+
as.boolean('1') // true
|
|
119
|
+
|
|
120
|
+
as.time('500ms') // 500
|
|
121
|
+
as.time('30s') // 30000
|
|
122
|
+
as.time('5m') // 300000
|
|
123
|
+
as.time('2h') // 7200000
|
|
124
|
+
as.time('1d') // 86400000
|
|
125
|
+
|
|
126
|
+
as.date('2024-12-31') // Date object
|
|
127
|
+
as.array('a,b,c', ',') // ['a', 'b', 'c']
|
|
128
|
+
as.json('{"x":1}') // { x: 1 }
|
|
129
|
+
as.base64('dGVzdA==') // 'test'
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Invalid values return `TypeError` for deferred validation:
|
|
94
133
|
|
|
95
134
|
```typescript
|
|
96
|
-
|
|
135
|
+
const config = {
|
|
136
|
+
port: as.number(process.env.PORT), // TypeError if invalid
|
|
137
|
+
host: as.string(process.env.HOST),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Errors surface with clear paths
|
|
141
|
+
ascertain({ port: Number, host: String }, config);
|
|
142
|
+
// → TypeError: "Invalid value undefined, expected non-nullable"
|
|
143
|
+
```
|
|
97
144
|
|
|
145
|
+
## Patterns
|
|
146
|
+
|
|
147
|
+
### Batch Validation
|
|
148
|
+
|
|
149
|
+
Compile once, validate many:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
const validateUser = compile(userSchema);
|
|
153
|
+
|
|
154
|
+
const results = users.map((user, i) => {
|
|
155
|
+
if (validateUser(user)) {
|
|
156
|
+
return { index: i, valid: true };
|
|
157
|
+
}
|
|
158
|
+
return { index: i, valid: false, error: validateUser.issues[0].message };
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Discriminated Unions
|
|
163
|
+
|
|
164
|
+
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:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { compile, discriminated } from 'ascertain';
|
|
168
|
+
|
|
169
|
+
const messageSchema = discriminated([
|
|
170
|
+
{ type: 'email', address: String },
|
|
171
|
+
{ type: 'sms', phone: String },
|
|
172
|
+
{ type: 'push', token: String },
|
|
173
|
+
], 'type');
|
|
174
|
+
|
|
175
|
+
const validate = compile(messageSchema);
|
|
176
|
+
|
|
177
|
+
validate({ type: 'email', address: 'user@example.com' }); // true
|
|
178
|
+
validate({ type: 'sms', phone: '123456' }); // true
|
|
179
|
+
validate({ type: 'push', token: 123 }); // false
|
|
180
|
+
validate({ type: 'unknown' }); // false
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Discriminant values must be string, number, or boolean literals.
|
|
184
|
+
|
|
185
|
+
### Conditional Rules
|
|
186
|
+
|
|
187
|
+
Use `or()` and `and()` for complex conditions:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
98
190
|
const schema = {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
191
|
+
type: or('email', 'sms'),
|
|
192
|
+
// Conditional: email requires address, sms requires phone
|
|
193
|
+
contact: or(
|
|
194
|
+
and({ type: 'email' }, { address: String }),
|
|
195
|
+
and({ type: 'sms' }, { phone: String }),
|
|
196
|
+
),
|
|
102
197
|
};
|
|
103
198
|
```
|
|
104
199
|
|
|
105
|
-
|
|
200
|
+
### Schema Composition
|
|
106
201
|
|
|
107
|
-
|
|
202
|
+
Build schemas from reusable parts:
|
|
108
203
|
|
|
109
204
|
```typescript
|
|
110
|
-
|
|
205
|
+
const addressSchema = {
|
|
206
|
+
street: String,
|
|
207
|
+
city: String,
|
|
208
|
+
zip: /^\d{5}$/,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const personSchema = {
|
|
212
|
+
name: String,
|
|
213
|
+
address: addressSchema,
|
|
214
|
+
};
|
|
111
215
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
as.number('0o77') // 63 (octal)
|
|
118
|
-
as.number('0b1010') // 10 (binary)
|
|
119
|
-
as.boolean('true') // true
|
|
120
|
-
as.boolean('1') // true
|
|
121
|
-
as.date('2024-12-31') // Date object
|
|
122
|
-
as.time('2m') // 120000 (milliseconds)
|
|
123
|
-
as.time('1h') // 3600000
|
|
124
|
-
as.array('a,b,c', ',') // ['a', 'b', 'c']
|
|
125
|
-
as.json('{"x":1}') // { x: 1 }
|
|
126
|
-
as.base64('dGVzdA==') // 'test'
|
|
216
|
+
const companySchema = {
|
|
217
|
+
name: String,
|
|
218
|
+
headquarters: addressSchema,
|
|
219
|
+
employees: [personSchema],
|
|
220
|
+
};
|
|
127
221
|
```
|
|
128
222
|
|
|
129
|
-
|
|
223
|
+
### Versioned Schemas
|
|
224
|
+
|
|
225
|
+
Version schemas as modules:
|
|
130
226
|
|
|
131
227
|
```typescript
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
228
|
+
// schemas/user.v1.ts
|
|
229
|
+
export const userSchemaV1 = { name: String, email: String };
|
|
230
|
+
|
|
231
|
+
// schemas/user.v2.ts
|
|
232
|
+
export const userSchemaV2 = {
|
|
233
|
+
...userSchemaV1,
|
|
234
|
+
phone: optional(String),
|
|
235
|
+
createdAt: Date,
|
|
135
236
|
};
|
|
136
237
|
|
|
137
|
-
//
|
|
138
|
-
|
|
238
|
+
// api/handler.ts
|
|
239
|
+
import { userSchemaV2 } from './schemas/user.v2';
|
|
240
|
+
const validate = compile(userSchemaV2);
|
|
139
241
|
```
|
|
140
242
|
|
|
141
|
-
|
|
243
|
+
### Config Validation
|
|
142
244
|
|
|
143
|
-
|
|
245
|
+
Validate only what each module needs:
|
|
144
246
|
|
|
145
247
|
```typescript
|
|
146
248
|
import { createValidator, as } from 'ascertain';
|
|
@@ -148,19 +250,71 @@ import { createValidator, as } from 'ascertain';
|
|
|
148
250
|
const config = {
|
|
149
251
|
app: { name: as.string(process.env.APP_NAME), port: as.number(process.env.PORT) },
|
|
150
252
|
db: { host: as.string(process.env.DB_HOST), pool: as.number(process.env.DB_POOL) },
|
|
151
|
-
|
|
253
|
+
cache: { ttl: as.time(process.env.CACHE_TTL) },
|
|
152
254
|
};
|
|
153
255
|
|
|
154
|
-
const validate = createValidator(config
|
|
256
|
+
const validate = createValidator(config);
|
|
155
257
|
|
|
156
258
|
// Each module validates only what it needs
|
|
157
|
-
const {
|
|
158
|
-
app: { name: String, port: Number },
|
|
259
|
+
const { db } = validate({
|
|
159
260
|
db: { host: String, pool: Number },
|
|
160
261
|
});
|
|
161
|
-
|
|
162
|
-
//
|
|
163
|
-
//
|
|
262
|
+
|
|
263
|
+
db.host; // string - validated and typed
|
|
264
|
+
db.pool; // number - validated and typed
|
|
265
|
+
// db.xxx // TypeScript error - property doesn't exist
|
|
266
|
+
|
|
267
|
+
// cache not validated = not accessible
|
|
268
|
+
// cache.ttl // TypeScript error - cache not in returned type
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Compile Options
|
|
272
|
+
|
|
273
|
+
By default `compile()` stops at the first validation error (fastest for invalid data). Pass `{ allErrors: true }` to collect all errors:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { compile } from 'ascertain';
|
|
277
|
+
|
|
278
|
+
const schema = { name: String, age: Number, active: Boolean };
|
|
279
|
+
|
|
280
|
+
// First-error mode (default) - stops at first failure
|
|
281
|
+
const validate = compile(schema);
|
|
282
|
+
if (!validate({ name: 123, age: 'bad', active: 'no' })) {
|
|
283
|
+
console.log(validate.issues.length); // 1
|
|
284
|
+
console.log(validate.issues[0].path); // ['name']
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// All-errors mode - collects every failure
|
|
288
|
+
const validateAll = compile(schema, { allErrors: true });
|
|
289
|
+
if (!validateAll({ name: 123, age: 'bad', active: 'no' })) {
|
|
290
|
+
console.log(validateAll.issues.length); // 3
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Standard Schema
|
|
295
|
+
|
|
296
|
+
Wrap a schema for [Standard Schema v1](https://standardschema.dev/) compliance, enabling interoperability with tRPC, TanStack Form, and other ecosystem libraries:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { standardSchema, or, optional } from 'ascertain';
|
|
300
|
+
|
|
301
|
+
const userValidator = standardSchema({
|
|
302
|
+
name: String,
|
|
303
|
+
age: Number,
|
|
304
|
+
role: or('admin', 'user'),
|
|
305
|
+
email: optional(String),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Use as regular validator (throws on error)
|
|
309
|
+
userValidator({ name: 'Alice', age: 30, role: 'admin' });
|
|
310
|
+
|
|
311
|
+
// Use Standard Schema interface (returns result object)
|
|
312
|
+
const result = userValidator['~standard'].validate(unknownData);
|
|
313
|
+
if (result.issues) {
|
|
314
|
+
console.log(result.issues);
|
|
315
|
+
} else {
|
|
316
|
+
console.log(result.value);
|
|
317
|
+
}
|
|
164
318
|
```
|
|
165
319
|
|
|
166
320
|
## Complete Example
|
|
@@ -169,55 +323,39 @@ const { app, db } = validate({
|
|
|
169
323
|
import { compile, or, optional, and, tuple, $keys, $values, $strict, as } from 'ascertain';
|
|
170
324
|
|
|
171
325
|
const schema = {
|
|
172
|
-
// Type validation
|
|
173
326
|
id: Number,
|
|
174
327
|
name: String,
|
|
175
|
-
active: Boolean,
|
|
176
|
-
|
|
177
|
-
// Pattern matching
|
|
178
328
|
email: /^[^@]+@[^@]+$/,
|
|
329
|
+
status: or('active', 'inactive', 'pending'),
|
|
330
|
+
role: optional(or('admin', 'user')),
|
|
179
331
|
|
|
180
|
-
// Union types
|
|
181
|
-
status: or('pending', 'active', 'disabled'),
|
|
182
|
-
|
|
183
|
-
// Optional fields
|
|
184
|
-
nickname: optional(String),
|
|
185
|
-
|
|
186
|
-
// Nested objects
|
|
187
332
|
profile: {
|
|
188
|
-
bio: String,
|
|
189
|
-
|
|
333
|
+
bio: optional(String),
|
|
334
|
+
avatar: optional(String),
|
|
190
335
|
},
|
|
191
336
|
|
|
192
|
-
|
|
193
|
-
createdAt: and(Date, { toISOString: Function }),
|
|
194
|
-
|
|
195
|
-
// Tuples
|
|
196
|
-
coordinates: tuple(Number, Number),
|
|
197
|
-
|
|
198
|
-
// Dynamic objects
|
|
199
|
-
metadata: {
|
|
337
|
+
settings: {
|
|
200
338
|
[$keys]: /^[a-z_]+$/,
|
|
201
|
-
[$values]: or(String, Number),
|
|
339
|
+
[$values]: or(String, Number, Boolean),
|
|
202
340
|
[$strict]: true,
|
|
203
341
|
},
|
|
204
342
|
|
|
205
|
-
|
|
206
|
-
|
|
343
|
+
coordinates: optional(tuple(Number, Number)),
|
|
344
|
+
createdAt: and(Date, { toISOString: Function }),
|
|
345
|
+
|
|
346
|
+
retries: as.number(process.env.MAX_RETRIES),
|
|
207
347
|
timeout: as.time(process.env.TIMEOUT),
|
|
208
348
|
};
|
|
209
349
|
|
|
210
|
-
const validate = compile(schema
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
350
|
+
const validate = compile(schema);
|
|
351
|
+
if (!validate(data)) {
|
|
352
|
+
console.error(validate.issues);
|
|
353
|
+
}
|
|
214
354
|
```
|
|
215
355
|
|
|
216
356
|
## License
|
|
217
357
|
|
|
218
|
-
[
|
|
219
|
-
|
|
220
|
-
Copyright (c) 2019-2026 Ivan Zakharchanka
|
|
358
|
+
[MIT](http://opensource.org/licenses/MIT) - Ivan Zakharchanka
|
|
221
359
|
|
|
222
360
|
[npm-url]: https://www.npmjs.com/package/ascertain
|
|
223
361
|
[downloads-image]: https://img.shields.io/npm/dw/ascertain.svg?maxAge=43200
|