@vertz/schema 0.2.0 → 0.2.1

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,75 +1,58 @@
1
1
  # @vertz/schema
2
2
 
3
- > Type-safe schema definition and validation for JavaScript/TypeScript
4
-
5
- A powerful validation library with end-to-end type inference, inspired by Zod. Define schemas with a fluent API and get full TypeScript types automatically.
6
-
7
- ## Prerequisites
8
-
9
- - **Node.js** 18+ or **Bun** 1.0+
10
- - **TypeScript** 5.0+
3
+ Type-safe validation with end-to-end type inference. Define schemas with a fluent API, get full TypeScript types automatically.
11
4
 
12
5
  ## Installation
13
6
 
14
7
  ```bash
15
- # npm
16
- npm install @vertz/schema
17
-
18
- # bun
19
8
  bun add @vertz/schema
20
9
  ```
21
10
 
22
11
  ## Quick Start
23
12
 
24
13
  ```typescript
25
- import { s } from '@vertz/schema';
14
+ import { s, type Infer } from '@vertz/schema';
26
15
 
27
16
  // Define a schema
28
17
  const userSchema = s.object({
29
18
  name: s.string().min(1),
30
- email: s.string().email(),
19
+ email: s.email(),
31
20
  age: s.number().int().min(18),
32
21
  });
33
22
 
34
- // Parse data (throws on invalid)
23
+ // Infer the type
24
+ type User = Infer<typeof userSchema>;
25
+ // { name: string; email: string; age: number }
26
+
27
+ // Parse (throws on invalid)
35
28
  const user = userSchema.parse({
36
29
  name: 'Alice',
37
30
  email: 'alice@example.com',
38
31
  age: 25,
39
32
  });
40
33
 
41
- // Safe parse (returns success/error)
42
- const result = userSchema.safeParse({
43
- name: 'Bob',
44
- email: 'not-an-email',
45
- age: 17,
46
- });
47
-
34
+ // Safe parse (never throws)
35
+ const result = userSchema.safeParse(data);
48
36
  if (result.success) {
49
- console.log('Valid user:', result.value);
37
+ console.log(result.data);
50
38
  } else {
51
- console.log('Validation errors:', result.error.issues);
39
+ console.log(result.error.issues);
52
40
  }
53
-
54
- // Type inference
55
- type User = typeof userSchema._output;
56
- // ✅ { name: string; email: string; age: number }
57
41
  ```
58
42
 
59
- ## Core Concepts
43
+ ## Schema Types
60
44
 
61
45
  ### Primitives
62
46
 
63
47
  ```typescript
64
- import { s } from '@vertz/schema';
65
-
66
48
  s.string() // string
67
49
  s.number() // number
68
50
  s.boolean() // boolean
69
51
  s.bigint() // bigint
70
52
  s.date() // Date
71
53
  s.symbol() // symbol
72
- s.int() // number (integer)
54
+ s.int() // number (integer-only)
55
+ s.nan() // NaN
73
56
  ```
74
57
 
75
58
  ### Special Types
@@ -83,74 +66,65 @@ s.void() // void
83
66
  s.never() // never
84
67
  ```
85
68
 
86
- ### Composite Types
69
+ ### Composites
87
70
 
88
71
  ```typescript
89
72
  // Objects
90
- const personSchema = s.object({
73
+ s.object({
91
74
  name: s.string(),
92
75
  age: s.number(),
93
- });
76
+ })
94
77
 
95
78
  // Arrays
96
- const numbersSchema = s.array(s.number());
79
+ s.array(s.number())
97
80
 
98
81
  // Tuples
99
- const pairSchema = s.tuple([s.string(), s.number()]);
82
+ s.tuple([s.string(), s.number()])
100
83
 
101
84
  // Enums
102
- const roleSchema = s.enum(['admin', 'user', 'guest']);
85
+ s.enum(['admin', 'user', 'guest'])
103
86
 
104
87
  // Literals
105
- const yesSchema = s.literal('yes');
88
+ s.literal('active')
106
89
 
107
90
  // Unions
108
- const statusSchema = s.union([
109
- s.literal('pending'),
110
- s.literal('approved'),
111
- s.literal('rejected'),
112
- ]);
91
+ s.union([s.string(), s.number()])
92
+
93
+ // Discriminated unions
94
+ s.discriminatedUnion('type', [
95
+ s.object({ type: s.literal('text'), content: s.string() }),
96
+ s.object({ type: s.literal('image'), url: s.url() }),
97
+ ])
98
+
99
+ // Intersections
100
+ s.intersection(
101
+ s.object({ id: s.string() }),
102
+ s.object({ name: s.string() }),
103
+ )
113
104
 
114
105
  // Records (dynamic keys)
115
- const configSchema = s.record(s.string());
106
+ s.record(s.string())
116
107
 
117
108
  // Maps
118
- const mapSchema = s.map(s.string(), s.number());
109
+ s.map(s.string(), s.number())
119
110
 
120
111
  // Sets
121
- const setSchema = s.set(s.string());
122
- ```
112
+ s.set(s.string())
123
113
 
124
- ### String Validations
114
+ // Files
115
+ s.file()
125
116
 
126
- ```typescript
127
- const schema = s.string()
128
- .min(3) // Min length
129
- .max(20) // Max length
130
- .length(10) // Exact length
131
- .regex(/^[a-z]+$/) // Pattern matching
132
- .startsWith('hello') // Prefix check
133
- .endsWith('world') // Suffix check
134
- .includes('mid') // Substring check
135
- .uppercase() // Must be all uppercase
136
- .lowercase() // Must be all lowercase
137
- .nonempty() // Alias for .min(1)
138
- .trim(); // Trim whitespace (transforms)
139
- ```
117
+ // Custom validators
118
+ s.custom<number>(
119
+ (val) => typeof val === 'number' && val % 2 === 0,
120
+ 'Must be an even number',
121
+ )
140
122
 
141
- ### Number Validations
123
+ // Instance checks
124
+ s.instanceof(Date)
142
125
 
143
- ```typescript
144
- const schema = s.number()
145
- .int() // Must be integer
146
- .positive() // > 0
147
- .negative() // < 0
148
- .nonpositive() // <= 0
149
- .nonnegative() // >= 0
150
- .min(0) // Minimum value
151
- .max(100) // Maximum value
152
- .multipleOf(5) // Must be divisible by n
153
- .finite(); // No Infinity or NaN
126
+ // Recursive types
127
+ s.lazy(() => categorySchema)
154
128
  ```
155
129
 
156
130
  ### Format Validators
@@ -159,151 +133,205 @@ Built-in validators for common formats:
159
133
 
160
134
  ```typescript
161
135
  s.email() // Email address
162
- s.uuid() // UUID (v1-v5)
136
+ s.uuid() // UUID
163
137
  s.url() // HTTP(S) URL
164
138
  s.hostname() // Valid hostname
165
139
  s.ipv4() // IPv4 address
166
140
  s.ipv6() // IPv6 address
167
141
  s.base64() // Base64 string
168
142
  s.hex() // Hexadecimal string
169
- s.jwt() // JWT token (format only, not verified)
143
+ s.jwt() // JWT token (format only)
170
144
  s.cuid() // CUID
171
145
  s.ulid() // ULID
172
146
  s.nanoid() // Nano ID
173
147
 
174
148
  // ISO formats
175
- s.iso.date() // ISO 8601 date (YYYY-MM-DD)
176
- s.iso.time() // ISO 8601 time (HH:MM:SS)
149
+ s.iso.date() // YYYY-MM-DD
150
+ s.iso.time() // HH:MM:SS
177
151
  s.iso.datetime() // ISO 8601 datetime
178
152
  s.iso.duration() // ISO 8601 duration (P1Y2M3D)
179
153
  ```
180
154
 
181
- ### Optional and Nullable
155
+ ### Database Enum Bridge
182
156
 
183
157
  ```typescript
184
- const schema = s.string().optional();
185
- // string | undefined
158
+ // Convert a @vertz/db enum column to a schema
159
+ s.fromDbEnum(statusColumn)
160
+ ```
186
161
 
187
- const schema2 = s.string().nullable();
188
- // string | null
162
+ ## Modifiers
189
163
 
190
- const schema3 = s.string().nullish();
191
- // string | null | undefined
164
+ ### Optional & Nullable
165
+
166
+ ```typescript
167
+ s.string().optional() // string | undefined
168
+ s.string().nullable() // string | null
169
+ s.string().nullable().optional() // string | null | undefined
192
170
  ```
193
171
 
194
172
  ### Default Values
195
173
 
196
174
  ```typescript
197
- const schema = s.string().default('hello');
175
+ s.string().default('hello')
176
+ s.number().default(() => Math.random())
198
177
 
199
- schema.parse(undefined); // 'hello'
200
- schema.parse('world'); // 'world'
178
+ s.string().default('hello').parse(undefined) // 'hello'
201
179
  ```
202
180
 
203
181
  ### Transformations
204
182
 
205
183
  ```typescript
206
- const schema = s.string().transform((val) => val.toUpperCase());
207
-
208
- schema.parse('hello'); // 'HELLO'
209
-
210
- // Chain transformations
211
- const trimmed = s.string().trim().transform((s) => s.toUpperCase());
212
- trimmed.parse(' hello '); // 'HELLO'
184
+ s.string().transform((val) => val.toUpperCase())
185
+ s.string().trim().transform((s) => s.split(','))
213
186
  ```
214
187
 
215
- ### Refinements (Custom Validation)
188
+ ### Refinements
216
189
 
217
190
  ```typescript
218
- const schema = s.string().refine(
191
+ // Simple predicate
192
+ s.string().refine(
219
193
  (val) => val.includes('@'),
220
194
  { message: 'Must contain @' },
221
- );
195
+ )
222
196
 
223
197
  // Multiple refinements
224
- const passwordSchema = s.string()
198
+ s.string()
225
199
  .min(8)
226
- .refine((val) => /[A-Z]/.test(val), {
227
- message: 'Must contain uppercase letter',
228
- })
229
- .refine((val) => /[0-9]/.test(val), {
230
- message: 'Must contain number',
231
- });
200
+ .refine((val) => /[A-Z]/.test(val), { message: 'Need uppercase' })
201
+ .refine((val) => /[0-9]/.test(val), { message: 'Need digit' })
232
202
  ```
233
203
 
234
- ### Super Refine (Access to Context)
204
+ ### Super Refine
205
+
206
+ Access the refinement context for cross-field validation:
235
207
 
236
208
  ```typescript
237
- const schema = s.object({
209
+ s.object({
238
210
  password: s.string(),
239
- confirmPassword: s.string(),
211
+ confirm: s.string(),
240
212
  }).superRefine((data, ctx) => {
241
- if (data.password !== data.confirmPassword) {
213
+ if (data.password !== data.confirm) {
242
214
  ctx.addIssue({
243
215
  code: 'custom',
244
- path: ['confirmPassword'],
216
+ path: ['confirm'],
245
217
  message: 'Passwords must match',
246
218
  });
247
219
  }
248
- });
220
+ })
249
221
  ```
250
222
 
251
223
  ### Branded Types
252
224
 
253
- Create nominal types that are structurally identical but semantically distinct:
254
-
255
225
  ```typescript
256
- const userIdSchema = s.string().uuid().brand('UserId');
257
- const postIdSchema = s.string().uuid().brand('PostId');
226
+ const UserId = s.string().uuid().brand('UserId');
227
+ const PostId = s.string().uuid().brand('PostId');
258
228
 
259
- type UserId = typeof userIdSchema._output; // string & Brand<'UserId'>
260
- type PostId = typeof postIdSchema._output; // string & Brand<'PostId'>
229
+ type UserId = Infer<typeof UserId>; // string & { __brand: 'UserId' }
230
+ type PostId = Infer<typeof PostId>; // string & { __brand: 'PostId' }
261
231
 
262
- // Type error: UserId and PostId are not assignable to each other
263
232
  function getUser(id: UserId) { /* ... */ }
264
- function getPost(id: PostId) { /* ... */ }
233
+ getUser(UserId.parse('...')); // OK
234
+ getUser(PostId.parse('...')); // Type error
235
+ ```
265
236
 
266
- const userId = userIdSchema.parse('...');
267
- getUser(userId); // ✅ OK
268
- getPost(userId); // ❌ Type error
237
+ ### Catch (Error Recovery)
238
+
239
+ ```typescript
240
+ s.number().catch(0).parse('invalid') // 0
269
241
  ```
270
242
 
271
243
  ### Readonly
272
244
 
273
- Mark types as readonly in the type system:
245
+ ```typescript
246
+ s.object({ tags: s.array(s.string()) }).readonly()
247
+ // Readonly<{ tags: readonly string[] }>
248
+ ```
249
+
250
+ ### Pipe
251
+
252
+ Chain schemas sequentially:
274
253
 
275
254
  ```typescript
276
- const schema = s.object({
277
- name: s.string(),
278
- tags: s.array(s.string()),
279
- }).readonly();
255
+ s.string().pipe(s.coerce.number())
256
+ ```
257
+
258
+ ## String Validations
280
259
 
281
- type Result = typeof schema._output;
282
- // Readonly<{ name: string; tags: readonly string[] }>
260
+ ```typescript
261
+ s.string()
262
+ .min(3) // min length
263
+ .max(20) // max length
264
+ .length(10) // exact length
265
+ .regex(/^[a-z]+$/) // pattern
266
+ .startsWith('hello')
267
+ .endsWith('world')
268
+ .includes('mid')
269
+ .uppercase() // must be uppercase
270
+ .lowercase() // must be lowercase
271
+ .trim() // trims whitespace (transform)
272
+ .toLowerCase() // converts to lowercase (transform)
273
+ .toUpperCase() // converts to uppercase (transform)
283
274
  ```
284
275
 
285
- ### Catch (Error Recovery)
276
+ ## Number Validations
277
+
278
+ ```typescript
279
+ s.number()
280
+ .int() // integer only
281
+ .positive() // > 0
282
+ .negative() // < 0
283
+ .nonnegative() // >= 0
284
+ .nonpositive() // <= 0
285
+ .min(0) // >= n
286
+ .max(100) // <= n
287
+ .gt(0) // > n
288
+ .lt(100) // < n
289
+ .multipleOf(5) // divisible by n
290
+ .finite() // no Infinity
291
+ ```
286
292
 
287
- Provide fallback values on parse errors:
293
+ ## Array Validations
288
294
 
289
295
  ```typescript
290
- const schema = s.number().catch(0);
296
+ s.array(s.string())
297
+ .min(1) // at least 1 element
298
+ .max(10) // at most 10 elements
299
+ .length(5) // exactly 5 elements
300
+ ```
291
301
 
292
- schema.parse(42); // 42
293
- schema.parse('invalid'); // 0 (caught and replaced)
302
+ ## Object Methods
303
+
304
+ ```typescript
305
+ const base = s.object({ id: s.string(), name: s.string(), email: s.email() });
306
+
307
+ base.pick('id', 'name') // { id: string; name: string }
308
+ base.omit('email') // { id: string; name: string }
309
+ base.partial() // { id?: string; name?: string; email?: string }
310
+ base.required() // all fields required
311
+ base.extend({ age: s.number() }) // add fields
312
+ base.merge(otherSchema) // merge two object schemas
313
+ base.strict() // reject unknown keys
314
+ base.passthrough() // pass through unknown keys
315
+ base.catchall(s.string()) // validate unknown keys with schema
316
+ base.keyof() // ['id', 'name', 'email']
317
+ ```
318
+
319
+ ## Tuple Rest Elements
320
+
321
+ ```typescript
322
+ s.tuple([s.string(), s.number()]).rest(s.boolean())
323
+ // [string, number, ...boolean[]]
294
324
  ```
295
325
 
296
326
  ## Parsing
297
327
 
298
328
  ### `.parse(data)`
299
329
 
300
- Parses and returns the data. Throws `ParseError` on validation failure.
330
+ Returns the parsed value. Throws `ParseError` on failure:
301
331
 
302
332
  ```typescript
303
- const schema = s.string();
304
-
305
333
  try {
306
- const value = schema.parse('hello'); // 'hello'
334
+ const value = schema.parse(data);
307
335
  } catch (error) {
308
336
  if (error instanceof ParseError) {
309
337
  console.log(error.issues);
@@ -313,328 +341,203 @@ try {
313
341
 
314
342
  ### `.safeParse(data)`
315
343
 
316
- Returns a result object with `success` boolean. Never throws.
344
+ Returns a result object. Never throws:
317
345
 
318
346
  ```typescript
319
- const schema = s.number();
320
-
321
- const result = schema.safeParse('42');
347
+ const result = schema.safeParse(data);
322
348
 
323
349
  if (result.success) {
324
- console.log(result.value); // number
350
+ result.data // parsed value
325
351
  } else {
326
- console.log(result.error.issues); // ValidationIssue[]
352
+ result.error // ParseError
353
+ result.error.issues // ValidationIssue[]
327
354
  }
328
355
  ```
329
356
 
330
357
  ## Type Inference
331
358
 
332
- ### Output Types (Parsed Value)
333
-
334
359
  ```typescript
335
- import type { Infer, Output } from '@vertz/schema';
336
-
337
- const schema = s.object({
338
- name: s.string(),
339
- age: s.number().optional(),
340
- });
341
-
342
- // All equivalent:
343
- type User1 = typeof schema._output;
344
- type User2 = Infer<typeof schema>;
345
- type User3 = Output<typeof schema>;
346
-
347
- // Result: { name: string; age?: number }
348
- ```
349
-
350
- ### Input Types (Before Parsing)
351
-
352
- Use `Input<T>` for the type before transformations:
353
-
354
- ```typescript
355
- import type { Input } from '@vertz/schema';
360
+ import type { Infer, Input, Output } from '@vertz/schema';
356
361
 
357
362
  const schema = s.string().transform((s) => s.length);
358
363
 
359
364
  type In = Input<typeof schema>; // string
360
365
  type Out = Output<typeof schema>; // number
366
+ type Out2 = Infer<typeof schema>; // number (alias for Output)
367
+
368
+ // Also available as instance properties:
369
+ type In3 = typeof schema._input;
370
+ type Out3 = typeof schema._output;
361
371
  ```
362
372
 
363
373
  ## Coercion
364
374
 
365
- Convert values to the target type:
375
+ Convert values to the target type before validation:
366
376
 
367
377
  ```typescript
368
- import { s } from '@vertz/schema';
369
-
370
- const schema = s.coerce.number();
378
+ s.coerce.string() // String(value)
379
+ s.coerce.number() // Number(value)
380
+ s.coerce.boolean() // Boolean(value)
381
+ s.coerce.bigint() // BigInt(value)
382
+ s.coerce.date() // new Date(value)
371
383
 
372
- schema.parse('42'); // 42 (string → number)
373
- schema.parse(42); // 42 (already number)
374
-
375
- // Available coercions:
376
- s.coerce.string() // → string
377
- s.coerce.number() // → number
378
- s.coerce.boolean() // → boolean
379
- s.coerce.bigint() // → bigint
380
- s.coerce.date() // → Date
384
+ s.coerce.number().parse('42') // 42
385
+ s.coerce.date().parse('2024-01-01') // Date object
381
386
  ```
382
387
 
383
- ## JSON Schema Generation
388
+ ## Error Handling
384
389
 
385
- Generate JSON Schema for interoperability:
390
+ ### ParseError
386
391
 
387
392
  ```typescript
388
- import { toJSONSchema } from '@vertz/schema';
393
+ import { ParseError } from '@vertz/schema';
389
394
 
390
- const schema = s.object({
391
- name: s.string().min(1),
392
- age: s.number().int().min(0),
393
- });
395
+ const result = schema.safeParse(data);
394
396
 
395
- const jsonSchema = toJSONSchema(schema);
396
- /*
397
- {
398
- type: 'object',
399
- properties: {
400
- name: { type: 'string', minLength: 1 },
401
- age: { type: 'integer', minimum: 0 }
402
- },
403
- required: ['name', 'age']
397
+ if (!result.success) {
398
+ for (const issue of result.error.issues) {
399
+ console.log(issue.code); // 'invalid_type', 'too_small', etc.
400
+ console.log(issue.message); // human-readable message
401
+ console.log(issue.path); // ['address', 'street']
402
+ }
404
403
  }
405
- */
406
404
  ```
407
405
 
408
- ## Schema Registry
409
-
410
- Register and reuse schemas by name:
406
+ ### Error Codes
411
407
 
412
408
  ```typescript
413
- import { SchemaRegistry } from '@vertz/schema';
409
+ import { ErrorCode } from '@vertz/schema';
414
410
 
415
- const registry = new SchemaRegistry();
416
-
417
- registry.register('User', s.object({
418
- id: s.string().uuid(),
419
- name: s.string(),
420
- }));
421
-
422
- registry.register('Post', s.object({
423
- id: s.string().uuid(),
424
- authorId: registry.ref('User').shape.id, // Reference other schemas
425
- title: s.string(),
426
- }));
427
-
428
- const userSchema = registry.get('User');
411
+ ErrorCode.InvalidType // 'invalid_type'
412
+ ErrorCode.TooSmall // 'too_small'
413
+ ErrorCode.TooBig // 'too_big'
414
+ ErrorCode.InvalidString // 'invalid_string'
415
+ ErrorCode.InvalidEnumValue // 'invalid_enum_value'
416
+ ErrorCode.InvalidLiteral // 'invalid_literal'
417
+ ErrorCode.InvalidUnion // 'invalid_union'
418
+ ErrorCode.InvalidDate // 'invalid_date'
419
+ ErrorCode.MissingProperty // 'missing_property'
420
+ ErrorCode.UnrecognizedKeys // 'unrecognized_keys'
421
+ ErrorCode.Custom // 'custom'
422
+ ErrorCode.NotMultipleOf // 'not_multiple_of'
423
+ ErrorCode.NotFinite // 'not_finite'
429
424
  ```
430
425
 
431
- ## Advanced Patterns
426
+ ## Result Type
432
427
 
433
- ### Discriminated Unions
428
+ Errors-as-values pattern for explicit error handling without try/catch:
434
429
 
435
430
  ```typescript
436
- const messageSchema = s.discriminatedUnion('type', [
437
- s.object({
438
- type: s.literal('text'),
439
- content: s.string(),
440
- }),
441
- s.object({
442
- type: s.literal('image'),
443
- url: s.url(),
444
- alt: s.string().optional(),
445
- }),
446
- ]);
431
+ import { ok, err, unwrap, map, flatMap, match, matchErr } from '@vertz/schema';
432
+ import type { Result, Ok, Err } from '@vertz/schema';
447
433
 
448
- type Message = typeof messageSchema._output;
449
- // { type: 'text'; content: string } | { type: 'image'; url: string; alt?: string }
450
- ```
434
+ // Create results
435
+ const success: Result<number, string> = ok(42);
436
+ const failure: Result<number, string> = err('not found');
451
437
 
452
- ### Recursive Types (with `lazy`)
453
-
454
- ```typescript
455
- interface Category {
456
- name: string;
457
- subcategories: Category[];
438
+ // Check and extract
439
+ if (success.ok) {
440
+ success.data // 42
441
+ }
442
+ if (failure.ok === false) {
443
+ failure.error // 'not found'
458
444
  }
459
445
 
460
- const categorySchema: s.Schema<Category> = s.object({
461
- name: s.string(),
462
- subcategories: s.lazy(() => s.array(categorySchema)),
463
- });
464
- ```
465
-
466
- ### Intersection
467
-
468
- ```typescript
469
- const baseSchema = s.object({ id: s.string() });
470
- const namedSchema = s.object({ name: s.string() });
471
-
472
- const userSchema = s.intersection(baseSchema, namedSchema);
473
- // { id: string; name: string }
474
- ```
446
+ // Unwrap (throws if Err)
447
+ const value = unwrap(success); // 42
475
448
 
476
- ### Custom Validators
449
+ // Map success value
450
+ const doubled = map(success, (n) => n * 2); // Ok(84)
477
451
 
478
- ```typescript
479
- const evenSchema = s.custom<number>(
480
- (val) => typeof val === 'number' && val % 2 === 0,
481
- 'Must be an even number',
452
+ // Chain Result-returning functions
453
+ const chained = flatMap(success, (n) =>
454
+ n > 0 ? ok(n.toString()) : err('must be positive')
482
455
  );
483
456
 
484
- evenSchema.parse(4); // 4
485
- evenSchema.parse(5); // ❌ throws
486
- ```
487
-
488
- ### File Validation
489
-
490
- ```typescript
491
- const imageSchema = s.file()
492
- .maxSize(5 * 1024 * 1024) // 5MB
493
- .mimeType(['image/png', 'image/jpeg', 'image/webp']);
457
+ // Pattern matching
458
+ const message = match(result, {
459
+ ok: (data) => `Got ${data}`,
460
+ err: (error) => `Failed: ${error}`,
461
+ });
494
462
 
495
- imageSchema.parse(file); // File object (browser or Node.js)
463
+ // Exhaustive error matching by code
464
+ const handled = matchErr(result, {
465
+ ok: (data) => data,
466
+ NOT_FOUND: (e) => fallback,
467
+ CONFLICT: (e) => retry(),
468
+ });
496
469
  ```
497
470
 
498
- ## Integration with @vertz/core
471
+ The `Result` type is used throughout `@vertz/db` for all query methods.
499
472
 
500
- Use schemas for request validation in vertz apps:
473
+ ## JSON Schema Generation
501
474
 
502
475
  ```typescript
503
- import { createModuleDef } from '@vertz/core';
504
- import { s } from '@vertz/schema';
505
-
506
- const moduleDef = createModuleDef({ name: 'users' });
476
+ import { toJSONSchema } from '@vertz/schema';
507
477
 
508
- const createUserSchema = s.object({
478
+ const schema = s.object({
509
479
  name: s.string().min(1),
510
- email: s.string().email(),
511
- age: s.number().int().min(18),
480
+ age: s.number().int().min(0),
512
481
  });
513
482
 
514
- const router = moduleDef.router({ prefix: '/users' });
483
+ const jsonSchema = toJSONSchema(schema);
484
+ // {
485
+ // type: 'object',
486
+ // properties: {
487
+ // name: { type: 'string', minLength: 1 },
488
+ // age: { type: 'integer', minimum: 0 }
489
+ // },
490
+ // required: ['name', 'age']
491
+ // }
515
492
 
516
- router.post('/', {
517
- body: createUserSchema,
518
- handler: (ctx) => {
519
- // ctx.body is fully typed as { name: string; email: string; age: number }
520
- const { name, email, age } = ctx.body;
521
- return { created: true, user: { name, email, age } };
522
- },
523
- });
493
+ // Also available as instance method:
494
+ schema.toJSONSchema()
524
495
  ```
525
496
 
526
- If validation fails, a `ValidationException` is automatically thrown with details.
497
+ ## Schema Registry
527
498
 
528
- ## Error Handling
499
+ Register and retrieve schemas by name:
529
500
 
530
501
  ```typescript
531
- import { ParseError } from '@vertz/schema';
532
-
533
- const result = schema.safeParse(data);
534
-
535
- if (!result.success) {
536
- const { error } = result;
537
-
538
- console.log(error.issues);
539
- /*
540
- [
541
- {
542
- code: 'invalid_type',
543
- expected: 'string',
544
- received: 'number',
545
- path: ['name'],
546
- message: 'Expected string, received number'
547
- },
548
- ...
549
- ]
550
- */
551
- }
552
- ```
553
-
554
- ## Comparison to Zod
555
-
556
- `@vertz/schema` is heavily inspired by Zod with similar API design:
557
-
558
- | Feature | @vertz/schema | Zod |
559
- |---------|--------------|-----|
560
- | Type inference | ✅ | ✅ |
561
- | Primitives | ✅ | ✅ |
562
- | Objects/Arrays | ✅ | ✅ |
563
- | Transformations | ✅ | ✅ |
564
- | Refinements | ✅ | ✅ |
565
- | Branded types | ✅ | ✅ |
566
- | JSON Schema export | ✅ | ✅ |
567
- | Schema registry | ✅ | ❌ |
568
- | Format validators | ✅ (built-in) | ❌ (plugin) |
569
- | ISO format methods | ✅ (`s.iso.*`) | ❌ |
570
-
571
- If you're familiar with Zod, you should feel right at home!
572
-
573
- ## API Reference
574
-
575
- ### Factory Functions
502
+ import { SchemaRegistry } from '@vertz/schema';
576
503
 
577
- All schemas are created via the `s` object:
504
+ // Register via .id()
505
+ const userSchema = s.object({ name: s.string() }).id('User');
578
506
 
579
- ```typescript
580
- import { s } from '@vertz/schema';
507
+ // Retrieve
508
+ const schema = SchemaRegistry.get('User');
509
+ SchemaRegistry.has('User'); // true
510
+ SchemaRegistry.getAll(); // Map<string, Schema>
581
511
  ```
582
512
 
583
- ### Schema Methods
584
-
585
- All schemas inherit these methods:
586
-
587
- - `.parse(data)` — Parse and return (throws on error)
588
- - `.safeParse(data)` — Parse and return `{ success, value?, error? }`
589
- - `.optional()` — Make schema optional (`T | undefined`)
590
- - `.nullable()` — Make schema nullable (`T | null`)
591
- - `.nullish()` — Make schema nullish (`T | null | undefined`)
592
- - `.default(value)` — Provide default value
593
- - `.transform(fn)` — Transform the value after validation
594
- - `.refine(fn, opts)` — Add custom validation
595
- - `.superRefine(fn)` — Add custom validation with context
596
- - `.brand<Brand>()` — Create branded type
597
- - `.readonly()` — Mark as readonly
598
- - `.catch(value)` — Provide fallback on error
599
- - `.optional()` — Alias for `.or(s.undefined())`
600
-
601
- ## TypeScript Tips
602
-
603
- ### Extracting Types
513
+ ## Schema Metadata
604
514
 
605
515
  ```typescript
606
- const schema = s.object({
607
- name: s.string(),
608
- age: s.number(),
609
- });
610
-
611
- // Extract output type
612
- type User = typeof schema._output;
516
+ const schema = s.string()
517
+ .id('Username')
518
+ .describe('The user display name')
519
+ .meta({ deprecated: true })
520
+ .example('alice');
613
521
 
614
- // Extract input type (before transforms)
615
- type UserInput = typeof schema._input;
522
+ schema.metadata.id // 'Username'
523
+ schema.metadata.description // 'The user display name'
524
+ schema.metadata.meta // { deprecated: true }
525
+ schema.metadata.examples // ['alice']
616
526
  ```
617
527
 
618
- ### Extending Schemas
528
+ ## Preprocessing
529
+
530
+ Transform raw input before schema validation:
619
531
 
620
532
  ```typescript
621
- const baseUserSchema = s.object({
622
- id: s.string().uuid(),
623
- createdAt: s.date(),
624
- });
533
+ import { preprocess } from '@vertz/schema';
625
534
 
626
- const userWithEmailSchema = baseUserSchema.extend({
627
- email: s.string().email(),
628
- });
535
+ const schema = preprocess(
536
+ (val) => typeof val === 'string' ? val.trim() : val,
537
+ s.string().min(1),
538
+ );
629
539
  ```
630
540
 
631
- ## Performance
632
-
633
- - Schema definitions are immutable and reusable
634
- - No code generation — pure runtime validation
635
- - Optimized for common cases (primitives, objects, arrays)
636
- - JSON Schema generation is cached
637
-
638
541
  ## License
639
542
 
640
543
  MIT
package/dist/index.d.ts CHANGED
@@ -56,6 +56,8 @@ declare class RefTracker {
56
56
  getDefs(): Record<string, JSONSchemaObject>;
57
57
  }
58
58
  declare function toJSONSchema2(schema: SchemaAny): JSONSchemaObject;
59
+ import { Err, Ok, Result } from "@vertz/errors";
60
+ import { err, flatMap, isErr, isOk, map, match, matchErr, ok, unwrap } from "@vertz/errors";
59
61
  declare enum SchemaType {
60
62
  String = "string",
61
63
  Number = "number",
@@ -93,13 +95,6 @@ interface SchemaMetadata {
93
95
  meta: Record<string, unknown> | undefined;
94
96
  examples: unknown[];
95
97
  }
96
- type SafeParseResult<T> = {
97
- success: true;
98
- data: T;
99
- } | {
100
- success: false;
101
- error: ParseError;
102
- };
103
98
  type SchemaAny = Schema<any, any>;
104
99
  /** Apply Readonly only to object types; leave primitives and `any` unchanged. */
105
100
  type ReadonlyOutput<O> = 0 extends 1 & O ? O : O extends object ? Readonly<O> : O;
@@ -119,8 +114,8 @@ declare abstract class Schema<
119
114
  abstract _schemaType(): SchemaType;
120
115
  abstract _toJSONSchema(tracker: RefTracker): JSONSchemaObject;
121
116
  abstract _clone(): Schema<O, I>;
122
- parse(value: unknown): O;
123
- safeParse(value: unknown): SafeParseResult<O>;
117
+ parse(value: unknown): Result<O, ParseError>;
118
+ safeParse(value: unknown): Result<O, ParseError>;
124
119
  id(name: string): this;
125
120
  describe(description: string): this;
126
121
  meta(data: Record<string, unknown>): this;
@@ -786,4 +781,4 @@ declare const s: {
786
781
  };
787
782
  };
788
783
  declare const schema: typeof s;
789
- export { toJSONSchema2 as toJSONSchema, schema, s, preprocess, VoidSchema, ValidationIssue, UuidSchema, UrlSchema, UnknownSchema, UnionSchema, UndefinedSchema, UlidSchema, TupleSchema, TransformSchema, SymbolSchema, SuperRefinedSchema, StringSchema, SetSchema, SchemaType, SchemaRegistry, SchemaMetadata, SchemaAny, Schema, SafeParseResult, RefinementContext, RefinedSchema, RefTracker, RecordSchema, ReadonlySchema, ReadonlyOutput, PipeSchema, ParseError, ParseContext, Output, OptionalSchema, ObjectSchema, NumberSchema, NullableSchema, NullSchema, NeverSchema, NanoidSchema, NanSchema, MapSchema, LiteralSchema, LazySchema, JwtSchema, JSONSchemaObject, IsoTimeSchema, IsoDurationSchema, IsoDatetimeSchema, IsoDateSchema, Ipv6Schema, Ipv4Schema, IntersectionSchema, InstanceOfSchema, Input, Infer, HostnameSchema, HexSchema, FileSchema, ErrorCode, EnumSchema, EmailSchema, DiscriminatedUnionSchema, DefaultSchema, DateSchema, CustomSchema, CuidSchema, CoercedStringSchema, CoercedNumberSchema, CoercedDateSchema, CoercedBooleanSchema, CoercedBigIntSchema, CatchSchema, BrandedSchema, BooleanSchema, BigIntSchema, Base64Schema, ArraySchema, AnySchema };
784
+ export { unwrap, toJSONSchema2 as toJSONSchema, schema, s, preprocess, ok, matchErr, match, map, isOk, isErr, flatMap, err, VoidSchema, ValidationIssue, UuidSchema, UrlSchema, UnknownSchema, UnionSchema, UndefinedSchema, UlidSchema, TupleSchema, TransformSchema, SymbolSchema, SuperRefinedSchema, StringSchema, SetSchema, SchemaType, SchemaRegistry, SchemaMetadata, SchemaAny, Schema, Result, RefinementContext, RefinedSchema, RefTracker, RecordSchema, ReadonlySchema, ReadonlyOutput, PipeSchema, ParseError, ParseContext, Output, OptionalSchema, Ok, ObjectSchema, NumberSchema, NullableSchema, NullSchema, NeverSchema, NanoidSchema, NanSchema, MapSchema, LiteralSchema, LazySchema, JwtSchema, JSONSchemaObject, IsoTimeSchema, IsoDurationSchema, IsoDatetimeSchema, IsoDateSchema, Ipv6Schema, Ipv4Schema, IntersectionSchema, InstanceOfSchema, Input, Infer, HostnameSchema, HexSchema, FileSchema, ErrorCode, Err, EnumSchema, EmailSchema, DiscriminatedUnionSchema, DefaultSchema, DateSchema, CustomSchema, CuidSchema, CoercedStringSchema, CoercedNumberSchema, CoercedDateSchema, CoercedBooleanSchema, CoercedBigIntSchema, CatchSchema, BrandedSchema, BooleanSchema, BigIntSchema, Base64Schema, ArraySchema, AnySchema };
package/dist/index.js CHANGED
@@ -102,6 +102,20 @@ function toJSONSchema(schema) {
102
102
  return schema.toJSONSchema();
103
103
  }
104
104
 
105
+ // src/result.ts
106
+ import {
107
+ err,
108
+ flatMap,
109
+ isErr,
110
+ isOk,
111
+ map,
112
+ match,
113
+ matchErr,
114
+ ok,
115
+ unwrap,
116
+ unwrapOr
117
+ } from "@vertz/errors";
118
+
105
119
  // src/core/schema.ts
106
120
  class Schema {
107
121
  _id;
@@ -115,21 +129,21 @@ class Schema {
115
129
  const ctx = new ParseContext;
116
130
  const result = this._runPipeline(value, ctx);
117
131
  if (ctx.hasIssues()) {
118
- throw new ParseError(ctx.issues);
132
+ return err(new ParseError(ctx.issues));
119
133
  }
120
- return result;
134
+ return ok(result);
121
135
  }
122
136
  safeParse(value) {
123
137
  const ctx = new ParseContext;
124
138
  try {
125
139
  const data = this._runPipeline(value, ctx);
126
140
  if (ctx.hasIssues()) {
127
- return { success: false, error: new ParseError(ctx.issues) };
141
+ return err(new ParseError(ctx.issues));
128
142
  }
129
- return { success: true, data };
143
+ return ok(data);
130
144
  } catch (e) {
131
145
  if (e instanceof ParseError) {
132
- return { success: false, error: e };
146
+ return err(e);
133
147
  }
134
148
  throw e;
135
149
  }
@@ -1440,10 +1454,10 @@ var IPV4_RE = /^(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})\.(0|[1-9]\d
1440
1454
  class Ipv4Schema extends FormatSchema {
1441
1455
  _errorMessage = "Invalid IPv4 address";
1442
1456
  _validate(value) {
1443
- const match = IPV4_RE.exec(value);
1444
- if (!match)
1457
+ const match2 = IPV4_RE.exec(value);
1458
+ if (!match2)
1445
1459
  return false;
1446
- return [match[1], match[2], match[3], match[4]].every((o) => Number(o) <= 255);
1460
+ return [match2[1], match2[2], match2[3], match2[4]].every((o) => Number(o) <= 255);
1447
1461
  }
1448
1462
  _jsonSchemaExtra() {
1449
1463
  return { format: "ipv4" };
@@ -1619,7 +1633,7 @@ class IntersectionSchema extends Schema {
1619
1633
  _parse(value, ctx) {
1620
1634
  const leftResult = this._left.safeParse(value);
1621
1635
  const rightResult = this._right.safeParse(value);
1622
- if (!leftResult.success || !rightResult.success) {
1636
+ if (!leftResult.ok || !rightResult.ok) {
1623
1637
  ctx.addIssue({
1624
1638
  code: "invalid_intersection" /* InvalidIntersection */,
1625
1639
  message: "Value does not satisfy intersection"
@@ -2254,7 +2268,7 @@ class UnionSchema extends Schema {
2254
2268
  _parse(value, ctx) {
2255
2269
  for (const option of this._options) {
2256
2270
  const result = option.safeParse(value);
2257
- if (result.success) {
2271
+ if (result.ok) {
2258
2272
  return result.data;
2259
2273
  }
2260
2274
  }
@@ -2377,10 +2391,19 @@ var s = {
2377
2391
  };
2378
2392
  var schema = s;
2379
2393
  export {
2394
+ unwrap,
2380
2395
  toJSONSchema,
2381
2396
  schema,
2382
2397
  s,
2383
2398
  preprocess,
2399
+ ok,
2400
+ matchErr,
2401
+ match,
2402
+ map,
2403
+ isOk,
2404
+ isErr,
2405
+ flatMap,
2406
+ err,
2384
2407
  VoidSchema,
2385
2408
  UuidSchema,
2386
2409
  UrlSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertz/schema",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Type-safe schema definitions for Vertz",
@@ -26,10 +26,14 @@
26
26
  ],
27
27
  "scripts": {
28
28
  "build": "bunup",
29
- "test": "vitest run",
29
+ "test": "bun test",
30
+ "test:coverage": "vitest run --coverage",
30
31
  "test:watch": "vitest",
31
32
  "typecheck": "tsc --noEmit"
32
33
  },
34
+ "dependencies": {
35
+ "@vertz/errors": "0.2.1"
36
+ },
33
37
  "devDependencies": {
34
38
  "@vitest/coverage-v8": "^4.0.18",
35
39
  "bunup": "latest",