@esmj/schema 0.3.6 → 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 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,16 +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
- 1. **TypeScript First**: Built with TypeScript in mind, it provides strong type inference and ensures type safety throughout your codebase.
16
- 2. **Extensibility**: Easily extend the library with custom logic using the `extend` function.
17
- 3. **Rich Features**: Includes advanced features like preprocessing, transformations, piping, and refinements, which are not always available in similar libraries.
18
- 4. **Lightweight**: None dependencies and a small footprint make it ideal for projects where performance and simplicity are key.
19
- 5. **Customizable**: Offers fine-grained control over validation and error handling.
20
- 6. **Performance**: `@esmj/schema` is optimized for speed, making it one of the fastest schema validation libraries available. Whether you're creating schemas, parsing data, or handling errors, `@esmj/schema` consistently outperforms many popular alternatives. Its minimalistic design ensures low overhead, even in high-performance applications.
97
+ 1. **TypeScript First**: Built with TypeScript in mind, it provides strong type inference—even for deeply nested and complex schemas.
98
+ 2. **Extensibility**: Easily extend the library with custom logic, refinements, and preprocessors using the `extend` function.
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.
101
+ 5. **Lightweight**: No dependencies and a small footprint make it ideal for projects where performance and simplicity are key.
102
+ 6. **Customizable**: Offers fine-grained control over validation, error handling, and schema composition.
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.
21
105
 
22
106
  ### Performance Highlights
23
107
 
@@ -33,7 +117,7 @@ When choosing a schema validation library, bundle size can be an important facto
33
117
 
34
118
  | Library | Bundle Size (minified + gzipped) |
35
119
  |-------------------|---------------------------------|
36
- | `@esmj/schema` | `~1,2 KB` |
120
+ | `@esmj/schema` | `~1,4 KB` |
37
121
  | Superstruct | ~3.2 KB |
38
122
  | @sinclair/typebox | ~11.7 KB |
39
123
  | Yup | ~12.2 KB |
@@ -138,6 +222,287 @@ console.log(result);
138
222
  // }
139
223
  ```
140
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
+
141
506
  ### Schema Types
142
507
 
143
508
  #### `s.string(options?)`
@@ -306,7 +671,7 @@ const preprocessSchema = s.preprocess((value) => new Date(value), s.date());
306
671
 
307
672
  ### Schema Methods
308
673
 
309
- #### `parse(value)`
674
+ #### `parse(value, parseOptions?)`
310
675
 
311
676
  Parses the given value according to the schema.
312
677
 
@@ -314,7 +679,7 @@ Parses the given value according to the schema.
314
679
  const result = stringSchema.parse('hello');
315
680
  ```
316
681
 
317
- #### `safeParse(value)`
682
+ #### `safeParse(value, parseOptions?)`
318
683
 
319
684
  Safely parses the given value according to the schema, returning a success or error result.
320
685
 
@@ -324,6 +689,10 @@ const result = stringSchema.safeParse('hello');
324
689
 
325
690
  const errorResult = stringSchema.safeParse(123);
326
691
  // { success: false, error: { message: 'The value "123" must be type of string but is type of "number".' } }
692
+
693
+ // Collect all errors (not just the first)
694
+ const allErrorsResult = stringSchema.safeParse(123, { abortEarly: false });
695
+ console.log(allErrorsResult.errors); // Array of all errors
327
696
  ```
328
697
 
329
698
  **Note:** The `error` returned by `safeParse` is not a native `Error` instance. Instead, it is a plain object with the following structure:
@@ -397,28 +766,211 @@ const refinedSchema = s.string().refine((val) => val.length <= 255, {
397
766
  });
398
767
  ```
399
768
 
769
+ #### Error Collection with `abortEarly` Option
770
+
771
+ Both `parse` and `safeParse` accept an optional second argument:
772
+ `parseOptions: { abortEarly?: boolean }`
773
+
774
+ - **`abortEarly`** (default: `true`):
775
+ If `true`, validation stops at the first error (previous behavior).
776
+ If `false`, all validation errors are collected and returned in the `errors` array.
777
+
778
+ **Example:**
779
+
780
+ ```typescript
781
+ const schema = s.object({
782
+ name: s.string(),
783
+ age: s.number(),
784
+ email: s.string()
785
+ });
786
+
787
+ // Default behavior (abortEarly: true)
788
+ const result1 = schema.safeParse({
789
+ name: 123,
790
+ age: 'not a number',
791
+ email: 42
792
+ });
793
+ console.log(result1.success); // false
794
+ console.log(result1.errors.length); // 1
795
+
796
+ // Collect all errors (abortEarly: false)
797
+ const result2 = schema.safeParse({
798
+ name: 123,
799
+ age: 'not a number',
800
+ email: 42
801
+ }, { abortEarly: false });
802
+ console.log(result2.success); // false
803
+ console.log(result2.errors.length); // 3
804
+ ```
805
+
806
+ **Error Result Structure:**
807
+
808
+ - `error`: The first error encountered (for compatibility)
809
+ - `errors`: Array of all errors (when `abortEarly: false`)
810
+
811
+ **Note:**
812
+ The `abortEarly` option is propagated through nested schemas, arrays, unions, and refinements.
813
+ This means you get all errors from deeply nested structures when using `{ abortEarly: false }`.
814
+
815
+ **Example Output:**
816
+
817
+ ```json
818
+ {
819
+ "success": false,
820
+ "error": {
821
+ "message": "Error parsing key \"name\": The value \"123\" must be type of string but is type of \"number\".",
822
+ "cause": { "key": "name" }
823
+ },
824
+ "errors": [
825
+ { "message": "Error parsing key \"name\": ...", "cause": { "key": "name" } },
826
+ { "message": "Error parsing key \"age\": ...", "cause": { "key": "age" } },
827
+ { "message": "Error parsing key \"email\": ...", "cause": { "key": "email" } }
828
+ ]
829
+ }
830
+ ```
831
+
400
832
  ### Extending Schemas
401
833
 
402
- You can extend the schema system with custom logic.
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:
403
887
 
404
888
  ```typescript
405
- import { extend, type StringSchemaInterface } from '@esmj/schema';
889
+ const userSchema = s.object({
890
+ name: s.string().trim(),
891
+ email: s.string().email(),
892
+ website: s.string().url().optional()
893
+ });
406
894
 
407
- interface StringSchemaInterface {
408
- customMethod(value: string): string {}
895
+ // Valid data
896
+ userSchema.parse({
897
+ name: ' John Doe ', // Will be trimmed
898
+ email: 'john@example.com'
899
+ });
900
+
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"
409
909
  }
910
+ ```
410
911
 
411
- extend((schema, validation, options) => {
412
- schema.customMethod = (value) => {
413
- // Custom logic
912
+ #### Advanced Extensions
414
913
 
415
- return value;
416
- };
914
+ You can extend any schema type and add complex validations:
417
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
+
418
968
  return schema;
419
969
  });
420
970
  ```
421
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
+
422
974
  ### More Examples
423
975
 
424
976
  #### Nested Objects
@@ -585,6 +1137,166 @@ console.log(result);
585
1137
  // 'HELLO'
586
1138
  ```
587
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
+
588
1300
  ## License
589
1301
 
590
1302
  MIT