@esmj/schema 0.4.0 → 0.5.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 +657 -13
- package/dist/array.cjs +1 -0
- package/dist/array.d.cts +13 -0
- package/dist/array.d.ts +13 -0
- package/dist/array.js +1 -0
- package/dist/chunk-5ARMWSHU.js +1 -0
- package/dist/chunk-6ABUMTDR.js +1 -0
- package/dist/chunk-JFGD5PND.js +1 -0
- package/dist/chunk-QEBUM44M.js +1 -0
- package/dist/full.cjs +1 -0
- package/dist/full.d.cts +4 -0
- package/dist/full.d.ts +4 -0
- package/dist/full.js +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +270 -0
- package/dist/index.d.ts +193 -3
- package/dist/index.js +1 -1
- package/dist/number.cjs +1 -0
- package/dist/number.d.cts +14 -0
- package/dist/number.d.ts +14 -0
- package/dist/number.js +1 -0
- package/dist/string.cjs +1 -0
- package/dist/string.d.cts +19 -0
- package/dist/string.d.ts +19 -0
- package/dist/string.js +1 -0
- package/examples/advanced-forms.ts +138 -0
- package/examples/basic-usage.ts +41 -0
- package/examples/custom-extensions.ts +120 -0
- package/examples/custom-validation.ts +110 -0
- package/package.json +40 -10
- package/tsconfig.json +12 -0
- package/dist/index.d.mts +0 -80
- package/dist/index.mjs +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
# Schema
|
|
2
2
|
|
|
3
|
-
This small library provides a simple schema validation system for JavaScript/TypeScript. The library has basic types with opportunities for extending.
|
|
3
|
+
This small library provides a simple schema validation system for JavaScript/TypeScript. The library has basic types with opportunities for extending.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Quick Start](#quick-start)
|
|
9
|
+
- [Why Use @esmj/schema?](#why-use-esmjschema)
|
|
10
|
+
- [Comparison with Similar Libraries](#comparison-with-similar-libraries)
|
|
11
|
+
- [Usage](#usage)
|
|
12
|
+
- [Basic Usage](#basic-usage)
|
|
13
|
+
- [Modular Extensions](#modular-extensions)
|
|
14
|
+
- [String Extensions](#string-extensions-esmjschemastring)
|
|
15
|
+
- [Number Extensions](#number-extensions-esmjschemanumber)
|
|
16
|
+
- [Array Extensions](#array-extensions-esmjschemaarray)
|
|
17
|
+
- [Full Extensions](#full-extensions-esmjschemafull)
|
|
18
|
+
- [API Reference Summary](#api-reference-summary)
|
|
19
|
+
- [Schema Types](#schema-types)
|
|
20
|
+
- [Schema Methods](#schema-methods)
|
|
21
|
+
- [parse](#parsevalue-parseoptions)
|
|
22
|
+
- [safeParse](#safeparsevalue-parseoptions)
|
|
23
|
+
- [Error Collection with abortEarly](#error-collection-with-abortearly-option)
|
|
24
|
+
- [Extending Schemas](#extending-schemas)
|
|
25
|
+
- [More Examples](#more-examples)
|
|
26
|
+
- [Examples Folder](#examples-folder)
|
|
27
|
+
- [Migration Guide](#migration-guide)
|
|
28
|
+
- [From Zod](#from-zod)
|
|
29
|
+
- [From Yup](#from-yup)
|
|
30
|
+
- [License](#license)
|
|
4
31
|
|
|
5
32
|
## Installation
|
|
6
33
|
|
|
@@ -8,17 +35,73 @@ This small library provides a simple schema validation system for JavaScript/Typ
|
|
|
8
35
|
npm install @esmj/schema
|
|
9
36
|
```
|
|
10
37
|
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
Get started with `@esmj/schema` in seconds:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { s } from '@esmj/schema';
|
|
44
|
+
|
|
45
|
+
// Define a schema
|
|
46
|
+
const userSchema = s.object({
|
|
47
|
+
name: s.string(),
|
|
48
|
+
age: s.number(),
|
|
49
|
+
email: s.string().optional()
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Parse data
|
|
53
|
+
const user = userSchema.parse({
|
|
54
|
+
name: 'John Doe',
|
|
55
|
+
age: 30
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
console.log(user);
|
|
59
|
+
// { name: 'John Doe', age: 30 }
|
|
60
|
+
|
|
61
|
+
// Safe parse with error handling
|
|
62
|
+
const result = userSchema.safeParse({
|
|
63
|
+
name: 'Jane',
|
|
64
|
+
age: 'invalid'
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (result.success) {
|
|
68
|
+
console.log(result.data);
|
|
69
|
+
} else {
|
|
70
|
+
console.error(result.error.message);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**With Extensions:**
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { s } from '@esmj/schema/full';
|
|
78
|
+
|
|
79
|
+
const schema = s.object({
|
|
80
|
+
username: s.string().trim().toLowerCase().min(3).max(20),
|
|
81
|
+
age: s.number().int().positive().min(18),
|
|
82
|
+
tags: s.array(s.string()).min(1).unique()
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const result = schema.parse({
|
|
86
|
+
username: ' JohnDoe ',
|
|
87
|
+
age: 25,
|
|
88
|
+
tags: ['developer', 'typescript']
|
|
89
|
+
});
|
|
90
|
+
// { username: 'johndoe', age: 25, tags: ['developer', 'typescript'] }
|
|
91
|
+
```
|
|
92
|
+
|
|
11
93
|
## Why Use `@esmj/schema`?
|
|
12
94
|
|
|
13
95
|
`@esmj/schema` is a lightweight and flexible schema validation library designed for developers who need a simple yet powerful way to validate and transform data. Here are some reasons to choose this package:
|
|
14
96
|
|
|
15
97
|
1. **TypeScript First**: Built with TypeScript in mind, it provides strong type inference—even for deeply nested and complex schemas.
|
|
16
98
|
2. **Extensibility**: Easily extend the library with custom logic, refinements, and preprocessors using the `extend` function.
|
|
17
|
-
3. **Rich Features**: Includes advanced features like preprocessing, transformations, piping, refinements, and robust error collection (`abortEarly`).
|
|
18
|
-
4. **Actionable Error Handling**: Collect all validation errors at once for better debugging and user experience, with clear and consistent error structures
|
|
99
|
+
3. **Rich Features**: Includes advanced features like preprocessing, transformations, piping, refinements, and robust error collection (`abortEarly`), which are not always available in similar libraries.
|
|
100
|
+
4. **Actionable Error Handling**: Collect all validation errors at once for better debugging and user experience, with clear and consistent error structures.
|
|
19
101
|
5. **Lightweight**: No dependencies and a small footprint make it ideal for projects where performance and simplicity are key.
|
|
20
102
|
6. **Customizable**: Offers fine-grained control over validation, error handling, and schema composition.
|
|
21
103
|
7. **Performance**: Optimized for speed, making it one of the fastest schema validation libraries available.
|
|
104
|
+
8. **Modular**: Import only what you need with separate string, number, and array extension modules to minimize bundle size.
|
|
22
105
|
|
|
23
106
|
### Performance Highlights
|
|
24
107
|
|
|
@@ -34,7 +117,7 @@ When choosing a schema validation library, bundle size can be an important facto
|
|
|
34
117
|
|
|
35
118
|
| Library | Bundle Size (minified + gzipped) |
|
|
36
119
|
|-------------------|---------------------------------|
|
|
37
|
-
| `@esmj/schema` | `~1,
|
|
120
|
+
| `@esmj/schema` | `~1,4 KB` |
|
|
38
121
|
| Superstruct | ~3.2 KB |
|
|
39
122
|
| @sinclair/typebox | ~11.7 KB |
|
|
40
123
|
| Yup | ~12.2 KB |
|
|
@@ -139,6 +222,287 @@ console.log(result);
|
|
|
139
222
|
// }
|
|
140
223
|
```
|
|
141
224
|
|
|
225
|
+
## Modular Extensions
|
|
226
|
+
|
|
227
|
+
`@esmj/schema` provides modular extensions that can be imported individually or all together, allowing you to include only the validation helpers you need.
|
|
228
|
+
|
|
229
|
+
### Import Options
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// Minimal version (core only, ~1.4 KB)
|
|
233
|
+
import { s } from '@esmj/schema';
|
|
234
|
+
|
|
235
|
+
// Full version (all extensions included, ~4 KB)
|
|
236
|
+
import { s } from '@esmj/schema/full';
|
|
237
|
+
|
|
238
|
+
// String extensions only
|
|
239
|
+
import { s } from '@esmj/schema/string';
|
|
240
|
+
|
|
241
|
+
// Number extensions only
|
|
242
|
+
import { s } from '@esmj/schema/number';
|
|
243
|
+
|
|
244
|
+
// Array extensions only
|
|
245
|
+
import { s } from '@esmj/schema/array';
|
|
246
|
+
|
|
247
|
+
// Mix and match (side-effect imports)
|
|
248
|
+
import '@esmj/schema/string';
|
|
249
|
+
import '@esmj/schema/number';
|
|
250
|
+
import { s } from '@esmj/schema';
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Bundle Size Impact
|
|
254
|
+
|
|
255
|
+
- **Core only** (`@esmj/schema`): ~1.4 KB gzipped
|
|
256
|
+
- **String extensions** (`@esmj/schema/string`): +~0.8 KB
|
|
257
|
+
- **Number extensions** (`@esmj/schema/number`): +~0.6 KB
|
|
258
|
+
- **Array extensions** (`@esmj/schema/array`): +~0.5 KB
|
|
259
|
+
- **Full** (`@esmj/schema/full`): ~4 KB gzipped (all extensions)
|
|
260
|
+
|
|
261
|
+
**Recommendation:** Import only the extensions you need to minimize bundle size.
|
|
262
|
+
|
|
263
|
+
### String Extensions (`@esmj/schema/string`)
|
|
264
|
+
|
|
265
|
+
String extensions provide common validation and transformation methods for string schemas.
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
import { s } from '@esmj/schema/string';
|
|
269
|
+
|
|
270
|
+
const userSchema = s.object({
|
|
271
|
+
username: s.string()
|
|
272
|
+
.trim() // Remove whitespace
|
|
273
|
+
.toLowerCase() // Convert to lowercase
|
|
274
|
+
.min(3) // Minimum 3 characters
|
|
275
|
+
.max(20) // Maximum 20 characters
|
|
276
|
+
.startsWith('user_'), // Must start with 'user_'
|
|
277
|
+
|
|
278
|
+
email: s.string()
|
|
279
|
+
.trim()
|
|
280
|
+
.toLowerCase()
|
|
281
|
+
.includes('@') // Must contain '@'
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
userSchema.parse({
|
|
285
|
+
username: ' USER_John ',
|
|
286
|
+
email: ' John@Example.com '
|
|
287
|
+
});
|
|
288
|
+
// ✓ { username: 'user_john', email: 'john@example.com' }
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Available String Methods:**
|
|
292
|
+
|
|
293
|
+
- **Length validations**: `min(length)`, `max(length)`, `length(exact)`, `nonEmpty()`
|
|
294
|
+
- **Pattern validations**: `startsWith(prefix)`, `endsWith(suffix)`, `includes(substring)`
|
|
295
|
+
- **Transformations**: `trim()`, `toLowerCase()`, `toUpperCase()`, `padStart(length, char)`, `padEnd(length, char)`, `replace(search, replace)`
|
|
296
|
+
|
|
297
|
+
### Number Extensions (`@esmj/schema/number`)
|
|
298
|
+
|
|
299
|
+
Number extensions provide validation methods for number schemas including range checks and type validations.
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import { s } from '@esmj/schema/number';
|
|
303
|
+
|
|
304
|
+
const productSchema = s.object({
|
|
305
|
+
price: s.number()
|
|
306
|
+
.positive() // Must be positive
|
|
307
|
+
.min(0.01) // Minimum value
|
|
308
|
+
.max(999999.99), // Maximum value
|
|
309
|
+
|
|
310
|
+
quantity: s.number()
|
|
311
|
+
.int() // Must be integer
|
|
312
|
+
.positive()
|
|
313
|
+
.min(1)
|
|
314
|
+
.max(1000),
|
|
315
|
+
|
|
316
|
+
discount: s.number()
|
|
317
|
+
.min(0)
|
|
318
|
+
.max(100)
|
|
319
|
+
.multipleOf(5) // Must be multiple of 5
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
productSchema.parse({
|
|
323
|
+
price: 29.99,
|
|
324
|
+
quantity: 5,
|
|
325
|
+
discount: 10
|
|
326
|
+
});
|
|
327
|
+
// ✓ { price: 29.99, quantity: 5, discount: 10 }
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Available Number Methods:**
|
|
331
|
+
|
|
332
|
+
- **Range validations**: `min(value)`, `max(value)`, `positive()`, `negative()`
|
|
333
|
+
- **Type validations**: `int()`, `float()`, `multipleOf(value)`, `finite()`
|
|
334
|
+
|
|
335
|
+
### Array Extensions (`@esmj/schema/array`)
|
|
336
|
+
|
|
337
|
+
Array extensions provide validation and transformation methods for array schemas.
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import { s } from '@esmj/schema/array';
|
|
341
|
+
|
|
342
|
+
const tagsSchema = s.object({
|
|
343
|
+
tags: s.array(s.string())
|
|
344
|
+
.min(1) // At least 1 item
|
|
345
|
+
.max(5) // At most 5 items
|
|
346
|
+
.unique() // All items must be unique
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
tagsSchema.parse({
|
|
350
|
+
tags: ['javascript', 'typescript', 'node']
|
|
351
|
+
});
|
|
352
|
+
// ✓ { tags: ['javascript', 'typescript', 'node'] }
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Available Array Methods:**
|
|
356
|
+
|
|
357
|
+
- **Size validations**: `min(length)`, `max(length)`, `length(exact)`, `nonEmpty()`
|
|
358
|
+
- **Content validations**: `unique()`
|
|
359
|
+
- **Transformations**: `sort()`, `reverse()`
|
|
360
|
+
|
|
361
|
+
### Full Extensions (`@esmj/schema/full`)
|
|
362
|
+
|
|
363
|
+
The full version includes all string, number, and array extensions in a single import.
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
import { s } from '@esmj/schema/full';
|
|
367
|
+
|
|
368
|
+
const productSchema = s.object({
|
|
369
|
+
// String extensions
|
|
370
|
+
name: s.string()
|
|
371
|
+
.trim()
|
|
372
|
+
.min(3)
|
|
373
|
+
.max(100),
|
|
374
|
+
|
|
375
|
+
sku: s.string()
|
|
376
|
+
.toUpperCase()
|
|
377
|
+
.length(8)
|
|
378
|
+
.startsWith('PROD'),
|
|
379
|
+
|
|
380
|
+
// Number extensions
|
|
381
|
+
price: s.number()
|
|
382
|
+
.positive()
|
|
383
|
+
.min(0.01)
|
|
384
|
+
.max(999999.99),
|
|
385
|
+
|
|
386
|
+
stock: s.number()
|
|
387
|
+
.int()
|
|
388
|
+
.min(0),
|
|
389
|
+
|
|
390
|
+
// Array extensions
|
|
391
|
+
categories: s.array(s.string())
|
|
392
|
+
.min(1)
|
|
393
|
+
.max(5)
|
|
394
|
+
.unique(),
|
|
395
|
+
|
|
396
|
+
dimensions: s.array(s.number().positive())
|
|
397
|
+
.length(3) // [length, width, height]
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Custom Error Messages:**
|
|
402
|
+
|
|
403
|
+
All extension methods support custom error messages:
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
const schema = s.object({
|
|
407
|
+
username: s.string().min(3, {
|
|
408
|
+
message: 'Username is too short! Please use at least 3 characters.'
|
|
409
|
+
}),
|
|
410
|
+
age: s.number().positive({
|
|
411
|
+
message: 'Age must be a positive number.'
|
|
412
|
+
}),
|
|
413
|
+
tags: s.array(s.string()).unique({
|
|
414
|
+
message: 'Duplicate tags are not allowed.'
|
|
415
|
+
})
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## API Reference Summary
|
|
420
|
+
|
|
421
|
+
### Core Types
|
|
422
|
+
|
|
423
|
+
- `s.string()` - String validation
|
|
424
|
+
- `s.number()` - Number validation
|
|
425
|
+
- `s.boolean()` - Boolean validation
|
|
426
|
+
- `s.date()` - Date validation
|
|
427
|
+
- `s.object(def)` - Object validation
|
|
428
|
+
- `s.array(def)` - Array validation
|
|
429
|
+
- `s.enum(values)` - Enum validation
|
|
430
|
+
- `s.union(schemas)` - Union validation
|
|
431
|
+
- `s.any()` - Any type
|
|
432
|
+
- `s.null()` - Null type
|
|
433
|
+
- `s.undefined()` - Undefined type
|
|
434
|
+
- `s.unknown()` - Unknown type
|
|
435
|
+
|
|
436
|
+
### Modifiers
|
|
437
|
+
|
|
438
|
+
- `.optional()` - Makes field optional
|
|
439
|
+
- `.nullable()` - Makes field nullable
|
|
440
|
+
- `.nullish()` - Makes field optional and nullable
|
|
441
|
+
- `.default(value)` - Sets default value
|
|
442
|
+
|
|
443
|
+
### Transformations
|
|
444
|
+
|
|
445
|
+
- `.transform(fn)` - Transform value
|
|
446
|
+
- `s.preprocess(fn, schema)` - Preprocess before validation
|
|
447
|
+
- `.pipe(schema)` - Pipe to another schema
|
|
448
|
+
- `.refine(fn, opts)` - Custom validation
|
|
449
|
+
|
|
450
|
+
### String Extensions
|
|
451
|
+
|
|
452
|
+
Available when importing from `@esmj/schema/string` or `@esmj/schema/full`:
|
|
453
|
+
|
|
454
|
+
**Length Validations:**
|
|
455
|
+
- `.min(n)` - Minimum length
|
|
456
|
+
- `.max(n)` - Maximum length
|
|
457
|
+
- `.length(n)` - Exact length
|
|
458
|
+
- `.nonEmpty()` - Non-empty string
|
|
459
|
+
|
|
460
|
+
**Pattern Validations:**
|
|
461
|
+
- `.startsWith(prefix)` - Must start with prefix
|
|
462
|
+
- `.endsWith(suffix)` - Must end with suffix
|
|
463
|
+
- `.includes(substring)` - Must contain substring
|
|
464
|
+
|
|
465
|
+
**Transformations:**
|
|
466
|
+
- `.trim()` - Remove whitespace
|
|
467
|
+
- `.toLowerCase()` - Convert to lowercase
|
|
468
|
+
- `.toUpperCase()` - Convert to uppercase
|
|
469
|
+
- `.padStart(length, char)` - Pad start
|
|
470
|
+
- `.padEnd(length, char)` - Pad end
|
|
471
|
+
- `.replace(search, replace)` - Replace text
|
|
472
|
+
|
|
473
|
+
### Number Extensions
|
|
474
|
+
|
|
475
|
+
Available when importing from `@esmj/schema/number` or `@esmj/schema/full`:
|
|
476
|
+
|
|
477
|
+
**Range Validations:**
|
|
478
|
+
- `.min(n)` - Minimum value
|
|
479
|
+
- `.max(n)` - Maximum value
|
|
480
|
+
- `.positive()` - Must be positive
|
|
481
|
+
- `.negative()` - Must be negative
|
|
482
|
+
|
|
483
|
+
**Type Validations:**
|
|
484
|
+
- `.int()` - Must be integer
|
|
485
|
+
- `.float()` - Must be float (non-integer)
|
|
486
|
+
- `.multipleOf(n)` - Must be multiple of n
|
|
487
|
+
- `.finite()` - Must be finite
|
|
488
|
+
|
|
489
|
+
### Array Extensions
|
|
490
|
+
|
|
491
|
+
Available when importing from `@esmj/schema/array` or `@esmj/schema/full`:
|
|
492
|
+
|
|
493
|
+
**Size Validations:**
|
|
494
|
+
- `.min(n)` - Minimum length
|
|
495
|
+
- `.max(n)` - Maximum length
|
|
496
|
+
- `.length(n)` - Exact length
|
|
497
|
+
- `.nonEmpty()` - Non-empty array
|
|
498
|
+
|
|
499
|
+
**Content Validations:**
|
|
500
|
+
- `.unique()` - All items must be unique
|
|
501
|
+
|
|
502
|
+
**Transformations:**
|
|
503
|
+
- `.sort()` - Sort array
|
|
504
|
+
- `.reverse()` - Reverse array
|
|
505
|
+
|
|
142
506
|
### Schema Types
|
|
143
507
|
|
|
144
508
|
#### `s.string(options?)`
|
|
@@ -467,26 +831,146 @@ This means you get all errors from deeply nested structures when using `{ abortE
|
|
|
467
831
|
|
|
468
832
|
### Extending Schemas
|
|
469
833
|
|
|
470
|
-
You can extend the schema system with custom
|
|
834
|
+
You can extend the schema system with custom validation methods. This is useful for adding domain-specific validations like email or URL formats.
|
|
835
|
+
|
|
836
|
+
#### Basic Extension Example
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
import { extend, type SchemaType, type StringSchemaInterface } from '@esmj/schema';
|
|
840
|
+
|
|
841
|
+
// First, declare the new methods you want to add
|
|
842
|
+
declare module '@esmj/schema' {
|
|
843
|
+
interface StringSchemaInterface {
|
|
844
|
+
email(): StringSchemaInterface;
|
|
845
|
+
url(): StringSchemaInterface;
|
|
846
|
+
trim(): StringSchemaInterface;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Define validation patterns
|
|
851
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
852
|
+
const URL_REGEX = /^(https?:\/\/[^\s$.?#].[^\s]*)$/;
|
|
853
|
+
|
|
854
|
+
// Extend the schema system
|
|
855
|
+
extend((schema: SchemaType, _, options) => {
|
|
856
|
+
// Only add methods to string schemas
|
|
857
|
+
if (options?.type === 'string') {
|
|
858
|
+
const stringSchema = schema as StringSchemaInterface;
|
|
859
|
+
|
|
860
|
+
// Add email validation
|
|
861
|
+
stringSchema.email = function() {
|
|
862
|
+
return this.refine((value) => EMAIL_REGEX.test(value), {
|
|
863
|
+
message: 'Invalid email format'
|
|
864
|
+
});
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
// Add URL validation
|
|
868
|
+
stringSchema.url = function() {
|
|
869
|
+
return this.refine((value) => URL_REGEX.test(value), {
|
|
870
|
+
message: 'Invalid URL format'
|
|
871
|
+
});
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
// Add string trimming
|
|
875
|
+
stringSchema.trim = function() {
|
|
876
|
+
return this.transform((value) => value.trim());
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return schema;
|
|
881
|
+
});
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
#### Usage of Extended Schemas
|
|
885
|
+
|
|
886
|
+
Once extended, you can use your custom methods in schema definitions:
|
|
471
887
|
|
|
472
888
|
```typescript
|
|
473
|
-
|
|
889
|
+
const userSchema = s.object({
|
|
890
|
+
name: s.string().trim(),
|
|
891
|
+
email: s.string().email(),
|
|
892
|
+
website: s.string().url().optional()
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
// Valid data
|
|
896
|
+
userSchema.parse({
|
|
897
|
+
name: ' John Doe ', // Will be trimmed
|
|
898
|
+
email: 'john@example.com'
|
|
899
|
+
});
|
|
474
900
|
|
|
475
|
-
|
|
476
|
-
|
|
901
|
+
// Invalid data
|
|
902
|
+
try {
|
|
903
|
+
userSchema.parse({
|
|
904
|
+
name: 'John Doe',
|
|
905
|
+
email: 'not-an-email'
|
|
906
|
+
});
|
|
907
|
+
} catch (error) {
|
|
908
|
+
console.error(error); // "Invalid email format"
|
|
477
909
|
}
|
|
910
|
+
```
|
|
478
911
|
|
|
479
|
-
|
|
480
|
-
schema.customMethod = (value) => {
|
|
481
|
-
// Custom logic
|
|
912
|
+
#### Advanced Extensions
|
|
482
913
|
|
|
483
|
-
|
|
484
|
-
};
|
|
914
|
+
You can extend any schema type and add complex validations:
|
|
485
915
|
|
|
916
|
+
```typescript
|
|
917
|
+
declare module '@esmj/schema' {
|
|
918
|
+
interface NumberSchemaInterface {
|
|
919
|
+
positive(): NumberSchemaInterface;
|
|
920
|
+
range(min: number, max: number): NumberSchemaInterface;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
interface ArraySchemaInterface<T> {
|
|
924
|
+
minLength(length: number): ArraySchemaInterface<T>;
|
|
925
|
+
unique(): ArraySchemaInterface<T>;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
extend((schema: SchemaType, _, options) => {
|
|
930
|
+
if (options?.type === 'number') {
|
|
931
|
+
const numberSchema = schema as NumberSchemaInterface;
|
|
932
|
+
|
|
933
|
+
numberSchema.positive = function() {
|
|
934
|
+
return this.refine((value) => value > 0, {
|
|
935
|
+
message: 'Number must be positive'
|
|
936
|
+
});
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
numberSchema.range = function(min, max) {
|
|
940
|
+
return this.refine((value) => value >= min && value <= max, {
|
|
941
|
+
message: `Number must be between ${min} and ${max}`
|
|
942
|
+
});
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (options?.type === 'array') {
|
|
947
|
+
const arraySchema = schema as ArraySchemaInterface<unknown>;
|
|
948
|
+
|
|
949
|
+
arraySchema.minLength = function(length) {
|
|
950
|
+
return this.refine((value) => value.length >= length, {
|
|
951
|
+
message: `Array must contain at least ${length} items`
|
|
952
|
+
});
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
arraySchema.unique = function() {
|
|
956
|
+
return this.refine((value) => {
|
|
957
|
+
const seen = new Set();
|
|
958
|
+
return value.every(item => {
|
|
959
|
+
const serialized = JSON.stringify(item);
|
|
960
|
+
if (seen.has(serialized)) return false;
|
|
961
|
+
seen.add(serialized);
|
|
962
|
+
return true;
|
|
963
|
+
});
|
|
964
|
+
}, { message: 'Array items must be unique' });
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
486
968
|
return schema;
|
|
487
969
|
});
|
|
488
970
|
```
|
|
489
971
|
|
|
972
|
+
This extension system gives you the flexibility to create domain-specific validation rules while maintaining type safety and the fluent API style.
|
|
973
|
+
|
|
490
974
|
### More Examples
|
|
491
975
|
|
|
492
976
|
#### Nested Objects
|
|
@@ -653,6 +1137,166 @@ console.log(result);
|
|
|
653
1137
|
// 'HELLO'
|
|
654
1138
|
```
|
|
655
1139
|
|
|
1140
|
+
## Examples Folder
|
|
1141
|
+
|
|
1142
|
+
The `examples/` folder contains comprehensive, runnable examples demonstrating various use cases:
|
|
1143
|
+
|
|
1144
|
+
### Basic Usage (`examples/basic-usage.ts`)
|
|
1145
|
+
|
|
1146
|
+
Demonstrates the core validation features with strings, numbers, arrays, and unions:
|
|
1147
|
+
|
|
1148
|
+
```bash
|
|
1149
|
+
node --experimental-strip-types examples/basic-usage.ts
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
### Custom Validation (`examples/custom-validation.ts`)
|
|
1153
|
+
|
|
1154
|
+
Shows how to create custom validators for common use cases:
|
|
1155
|
+
- Email validation with regex
|
|
1156
|
+
- URL validation
|
|
1157
|
+
- Age range validation
|
|
1158
|
+
- Password strength validation
|
|
1159
|
+
- Cross-field validation (e.g., password confirmation)
|
|
1160
|
+
|
|
1161
|
+
```bash
|
|
1162
|
+
node --experimental-strip-types examples/custom-validation.ts
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
### Advanced Forms (`examples/advanced-forms.ts`)
|
|
1166
|
+
|
|
1167
|
+
Real-world form validation examples:
|
|
1168
|
+
- User profile schema with nested objects
|
|
1169
|
+
- Address validation with postal codes
|
|
1170
|
+
- Phone number formatting and validation
|
|
1171
|
+
- API response validation
|
|
1172
|
+
- Complex nested structures
|
|
1173
|
+
|
|
1174
|
+
```bash
|
|
1175
|
+
node --experimental-strip-types examples/advanced-forms.ts
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
### Custom Extensions (`examples/custom-extensions.ts`)
|
|
1179
|
+
|
|
1180
|
+
Demonstrates how to extend the library with custom methods:
|
|
1181
|
+
- Email validation extension
|
|
1182
|
+
- URL validation extension
|
|
1183
|
+
- UUID validation extension
|
|
1184
|
+
- Combining custom extensions with built-in validators
|
|
1185
|
+
|
|
1186
|
+
```bash
|
|
1187
|
+
node --experimental-strip-types examples/custom-extensions.ts
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
**To run all examples:**
|
|
1191
|
+
|
|
1192
|
+
```bash
|
|
1193
|
+
# Using Node.js with experimental type stripping (built-in, no dependencies)
|
|
1194
|
+
node --experimental-strip-types examples/basic-usage.ts
|
|
1195
|
+
node --experimental-strip-types examples/custom-validation.ts
|
|
1196
|
+
node --experimental-strip-types examples/advanced-forms.ts
|
|
1197
|
+
node --experimental-strip-types examples/custom-extensions.ts
|
|
1198
|
+
|
|
1199
|
+
# OR using tsx (requires installation)
|
|
1200
|
+
npm install -g tsx # If not already installed
|
|
1201
|
+
npx tsx examples/basic-usage.ts
|
|
1202
|
+
npx tsx examples/custom-validation.ts
|
|
1203
|
+
npx tsx examples/advanced-forms.ts
|
|
1204
|
+
npx tsx examples/custom-extensions.ts
|
|
1205
|
+
```
|
|
1206
|
+
## Migration Guide
|
|
1207
|
+
|
|
1208
|
+
### From Zod
|
|
1209
|
+
|
|
1210
|
+
`@esmj/schema` has a similar API to Zod, making migration straightforward:
|
|
1211
|
+
|
|
1212
|
+
```typescript
|
|
1213
|
+
// Zod
|
|
1214
|
+
import { z } from 'zod';
|
|
1215
|
+
|
|
1216
|
+
const userSchema = z.object({
|
|
1217
|
+
name: z.string().min(3).max(50),
|
|
1218
|
+
email: z.string().email(),
|
|
1219
|
+
age: z.number().positive().int(),
|
|
1220
|
+
role: z.enum(['admin', 'user']),
|
|
1221
|
+
tags: z.array(z.string()).optional()
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
// @esmj/schema (with extensions)
|
|
1225
|
+
import { s } from '@esmj/schema/full';
|
|
1226
|
+
|
|
1227
|
+
const userSchema = s.object({
|
|
1228
|
+
name: s.string().min(3).max(50),
|
|
1229
|
+
email: s.string(), // Note: email() validation requires custom extension
|
|
1230
|
+
age: s.number().positive().int(),
|
|
1231
|
+
role: s.enum(['admin', 'user']),
|
|
1232
|
+
tags: s.array(s.string()).optional()
|
|
1233
|
+
});
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
**Key Differences:**
|
|
1237
|
+
|
|
1238
|
+
| Feature | Zod | @esmj/schema |
|
|
1239
|
+
|---------|-----|--------------|
|
|
1240
|
+
| Import | `import { z } from 'zod'` | `import { s } from '@esmj/schema'` |
|
|
1241
|
+
| Extensions | Built-in | Modular (`/string`, `/number`, `/array`, `/full`) |
|
|
1242
|
+
| Bundle size | ~13 KB | ~1.4 KB (core), ~4 KB (full) |
|
|
1243
|
+
| Email validation | `.email()` built-in | Custom extension (see [Extending Schemas](#extending-schemas)) |
|
|
1244
|
+
| Error format | Native Error | Plain object `{ success, error, errors }` |
|
|
1245
|
+
|
|
1246
|
+
**Migration Tips:**
|
|
1247
|
+
|
|
1248
|
+
1. Replace `z` with `s` in your imports
|
|
1249
|
+
2. For string methods like `.min()`, `.trim()`, import from `@esmj/schema/full` or `@esmj/schema/string`
|
|
1250
|
+
3. Add custom extensions for email, URL validation (see examples below)
|
|
1251
|
+
4. Update error handling to use the plain object structure
|
|
1252
|
+
|
|
1253
|
+
### From Yup
|
|
1254
|
+
|
|
1255
|
+
Migrating from Yup requires a few adjustments in syntax:
|
|
1256
|
+
|
|
1257
|
+
```typescript
|
|
1258
|
+
// Yup
|
|
1259
|
+
import * as yup from 'yup';
|
|
1260
|
+
|
|
1261
|
+
const userSchema = yup.object({
|
|
1262
|
+
name: yup.string().required().min(3).max(50),
|
|
1263
|
+
email: yup.string().required().email(),
|
|
1264
|
+
age: yup.number().required().positive().integer(),
|
|
1265
|
+
website: yup.string().url().nullable(),
|
|
1266
|
+
tags: yup.array().of(yup.string()).min(1)
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
// @esmj/schema (with extensions)
|
|
1270
|
+
import { s } from '@esmj/schema/full';
|
|
1271
|
+
|
|
1272
|
+
const userSchema = s.object({
|
|
1273
|
+
name: s.string().min(3).max(50), // Fields are required by default
|
|
1274
|
+
email: s.string(), // Note: email() validation requires custom extension
|
|
1275
|
+
age: s.number().positive().int(),
|
|
1276
|
+
website: s.string().nullable(),
|
|
1277
|
+
tags: s.array(s.string()).min(1)
|
|
1278
|
+
});
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
**Key Differences:**
|
|
1282
|
+
|
|
1283
|
+
| Feature | Yup | @esmj/schema |
|
|
1284
|
+
|---------|-----|--------------|
|
|
1285
|
+
| Required fields | `.required()` explicit | Required by default |
|
|
1286
|
+
| Optional fields | Default behavior | `.optional()` explicit |
|
|
1287
|
+
| Array of type | `.array().of(type)` | `.array(type)` |
|
|
1288
|
+
| Integer | `.integer()` | `.int()` |
|
|
1289
|
+
| Email validation | `.email()` built-in | Custom extension needed |
|
|
1290
|
+
| Async validation | Supported | Not currently supported |
|
|
1291
|
+
|
|
1292
|
+
**Migration Tips:**
|
|
1293
|
+
|
|
1294
|
+
1. Remove `.required()` calls (fields are required by default)
|
|
1295
|
+
2. Add `.optional()` for optional fields
|
|
1296
|
+
3. Change `.array().of(type)` to `.array(type)`
|
|
1297
|
+
4. Change `.integer()` to `.int()`
|
|
1298
|
+
5. Add custom extensions for email, URL validation
|
|
1299
|
+
|
|
656
1300
|
## License
|
|
657
1301
|
|
|
658
1302
|
MIT
|