canonize 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +629 -0
  3. package/dist/coercers/array.d.ts +7 -0
  4. package/dist/coercers/array.d.ts.map +1 -0
  5. package/dist/coercers/bigint.d.ts +5 -0
  6. package/dist/coercers/bigint.d.ts.map +1 -0
  7. package/dist/coercers/boolean.d.ts +5 -0
  8. package/dist/coercers/boolean.d.ts.map +1 -0
  9. package/dist/coercers/date.d.ts +5 -0
  10. package/dist/coercers/date.d.ts.map +1 -0
  11. package/dist/coercers/discriminated-union.d.ts +7 -0
  12. package/dist/coercers/discriminated-union.d.ts.map +1 -0
  13. package/dist/coercers/enum.d.ts +9 -0
  14. package/dist/coercers/enum.d.ts.map +1 -0
  15. package/dist/coercers/intersection.d.ts +8 -0
  16. package/dist/coercers/intersection.d.ts.map +1 -0
  17. package/dist/coercers/literal.d.ts +5 -0
  18. package/dist/coercers/literal.d.ts.map +1 -0
  19. package/dist/coercers/map-set.d.ts +11 -0
  20. package/dist/coercers/map-set.d.ts.map +1 -0
  21. package/dist/coercers/nan.d.ts +5 -0
  22. package/dist/coercers/nan.d.ts.map +1 -0
  23. package/dist/coercers/null.d.ts +5 -0
  24. package/dist/coercers/null.d.ts.map +1 -0
  25. package/dist/coercers/number.d.ts +13 -0
  26. package/dist/coercers/number.d.ts.map +1 -0
  27. package/dist/coercers/object.d.ts +7 -0
  28. package/dist/coercers/object.d.ts.map +1 -0
  29. package/dist/coercers/record.d.ts +7 -0
  30. package/dist/coercers/record.d.ts.map +1 -0
  31. package/dist/coercers/string.d.ts +6 -0
  32. package/dist/coercers/string.d.ts.map +1 -0
  33. package/dist/coercers/tuple.d.ts +7 -0
  34. package/dist/coercers/tuple.d.ts.map +1 -0
  35. package/dist/coercers/union.d.ts +7 -0
  36. package/dist/coercers/union.d.ts.map +1 -0
  37. package/dist/constants.d.ts +36 -0
  38. package/dist/constants.d.ts.map +1 -0
  39. package/dist/create-schema.d.ts +5 -0
  40. package/dist/create-schema.d.ts.map +1 -0
  41. package/dist/index.d.ts +51 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +1623 -0
  44. package/dist/index.js.map +33 -0
  45. package/dist/tool-parameters/index.d.ts +206 -0
  46. package/dist/tool-parameters/index.d.ts.map +1 -0
  47. package/dist/tool-parameters/index.js +301 -0
  48. package/dist/tool-parameters/index.js.map +12 -0
  49. package/dist/tool-parameters/link-metadata.d.ts +17 -0
  50. package/dist/tool-parameters/link-metadata.d.ts.map +1 -0
  51. package/dist/utilities/circular.d.ts +27 -0
  52. package/dist/utilities/circular.d.ts.map +1 -0
  53. package/dist/utilities/parsers.d.ts +29 -0
  54. package/dist/utilities/parsers.d.ts.map +1 -0
  55. package/dist/utilities/special-methods.d.ts +14 -0
  56. package/dist/utilities/special-methods.d.ts.map +1 -0
  57. package/dist/utilities/type-coercion.d.ts +11 -0
  58. package/dist/utilities/type-coercion.d.ts.map +1 -0
  59. package/dist/utilities/type-detection.d.ts +26 -0
  60. package/dist/utilities/type-detection.d.ts.map +1 -0
  61. package/dist/utilities/type-guards.d.ts +35 -0
  62. package/dist/utilities/type-guards.d.ts.map +1 -0
  63. package/package.json +123 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Steve Kinney
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,629 @@
1
+ # canonize
2
+
3
+ **Aggressive type coercion for Zod schemas.**
4
+
5
+ ## The Problem
6
+
7
+ You've defined a beautiful Zod schema:
8
+
9
+ ```typescript
10
+ const userSchema = z.object({
11
+ age: z.number(),
12
+ active: z.boolean(),
13
+ tags: z.array(z.string()),
14
+ });
15
+ ```
16
+
17
+ Then reality hits. Your API receives:
18
+
19
+ ```typescript
20
+ { age: "30", active: "yes", tags: "admin,user" }
21
+ ```
22
+
23
+ Zod's built-in `z.coerce` helps with simple cases, but it won't parse `"yes"` as `true`, split `"admin,user"` into an array, or handle the dozen other formats your data might arrive in.
24
+
25
+ You're left writing preprocessing logic, custom transforms, or wrapper functions for every schema. The business logic gets buried under input normalization.
26
+
27
+ ## The Solution
28
+
29
+ Wrap your schema with `canonize()` and move on:
30
+
31
+ ```typescript
32
+ import { canonize } from 'canonize';
33
+
34
+ const userSchema = canonize(
35
+ z.object({
36
+ age: z.number(),
37
+ active: z.boolean(),
38
+ tags: z.array(z.string()),
39
+ }),
40
+ );
41
+
42
+ // All of these now work:
43
+ userSchema.parse({ age: '30', active: 'yes', tags: 'admin,user' });
44
+ userSchema.parse({ age: 30.5, active: 1, tags: ['admin'] });
45
+ userSchema.parse({ age: '30px', active: 'enabled', tags: '["admin"]' });
46
+ ```
47
+
48
+ Canonize handles the messy real-world inputs so your schema can focus on validation:
49
+
50
+ - `"30"`, `"30px"`, `"30.5"` → `30` (number)
51
+ - `"yes"`, `"true"`, `"on"`, `"1"`, `1` → `true` (boolean)
52
+ - `"admin,user"`, `'["admin","user"]'` → `["admin", "user"]` (array)
53
+ - `"2024-01-15"`, `1705276800000`, `"now"` → `Date` object
54
+ - Nested objects, unions, discriminated unions, intersections—all coerced recursively
55
+
56
+ ## When to Use Canonize
57
+
58
+ - **API endpoints** receiving form data, query strings, or JSON from unknown clients
59
+ - **Configuration files** where users write `enabled: yes` instead of `enabled: true`
60
+ - **LLM tool calls** where the model outputs `"42"` instead of `42`
61
+ - **Legacy system integration** with inconsistent data formats
62
+ - **CSV/spreadsheet imports** where everything is a string
63
+
64
+ ## Installation
65
+
66
+ ```bash
67
+ npm install canonize zod
68
+ # or
69
+ bun add canonize zod
70
+ # or
71
+ pnpm add canonize zod
72
+ ```
73
+
74
+ ## Quick Start
75
+
76
+ ```typescript
77
+ import { canonize } from 'canonize';
78
+ import { z } from 'zod';
79
+
80
+ // Wrap any Zod schema
81
+ const schema = canonize(
82
+ z.object({
83
+ name: z.string(),
84
+ age: z.number(),
85
+ active: z.boolean(),
86
+ tags: z.array(z.string()),
87
+ }),
88
+ );
89
+
90
+ // Coercion happens automatically
91
+ schema.parse({ name: 123, age: '30', active: 'yes', tags: 'a,b,c' });
92
+ // { name: '123', age: 30, active: true, tags: ['a', 'b', 'c'] }
93
+ ```
94
+
95
+ ---
96
+
97
+ ## API Reference
98
+
99
+ ### Core Function
100
+
101
+ #### `canonize<T>(schema: T): T`
102
+
103
+ Wraps a Zod schema with aggressive type coercion. Returns the same schema type for full TypeScript inference.
104
+
105
+ ```typescript
106
+ import { canonize } from 'canonize';
107
+ import { z } from 'zod';
108
+
109
+ const numberSchema = canonize(z.number());
110
+ numberSchema.parse('42'); // 42
111
+ numberSchema.parse('42px'); // 42
112
+ numberSchema.parse('1,234'); // 1234
113
+
114
+ const boolSchema = canonize(z.boolean());
115
+ boolSchema.parse('yes'); // true
116
+ boolSchema.parse('0'); // false
117
+ boolSchema.parse('enabled'); // true
118
+
119
+ const arraySchema = canonize(z.array(z.number()));
120
+ arraySchema.parse('1,2,3'); // [1, 2, 3]
121
+ arraySchema.parse('[1,2,3]'); // [1, 2, 3]
122
+ arraySchema.parse(42); // [42]
123
+ ```
124
+
125
+ **Supported Zod types:**
126
+
127
+ - Primitives: `string`, `number`, `boolean`, `bigint`, `date`, `null`, `nan`
128
+ - Collections: `array`, `object`, `tuple`, `record`, `map`, `set`
129
+ - Composites: `union`, `discriminatedUnion`, `intersection`
130
+ - Special: `enum`, `literal`, `any`, `unknown`, `custom`
131
+ - Wrappers: `optional`, `nullable`, `default`, `catch`, `readonly`, `lazy`
132
+
133
+ ---
134
+
135
+ ### Type Detection Utilities
136
+
137
+ #### `getZodTypeName(schema: ZodTypeAny): string`
138
+
139
+ Returns the Zod type name for a schema. Useful for building custom coercion logic.
140
+
141
+ ```typescript
142
+ import { getZodTypeName } from 'canonize';
143
+ import { z } from 'zod';
144
+
145
+ getZodTypeName(z.string()); // 'string'
146
+ getZodTypeName(z.array(z.number())); // 'array'
147
+ getZodTypeName(z.object({})); // 'object'
148
+ getZodTypeName(z.string().optional()); // 'optional'
149
+ ```
150
+
151
+ #### `unwrapSchema(schema: ZodTypeAny): ZodTypeAny`
152
+
153
+ Removes wrapper types (`optional`, `nullable`, `default`, `catch`, `readonly`) to get the inner schema.
154
+
155
+ ```typescript
156
+ import { unwrapSchema, getZodTypeName } from 'canonize';
157
+ import { z } from 'zod';
158
+
159
+ const wrapped = z.string().optional().nullable().default('hello');
160
+ const inner = unwrapSchema(wrapped);
161
+ getZodTypeName(inner); // 'string'
162
+ ```
163
+
164
+ ---
165
+
166
+ ### Circular Reference Tracking
167
+
168
+ #### `CircularTracker`
169
+
170
+ A `WeakSet`-based tracker for detecting circular references during coercion. Prevents infinite loops when processing self-referential data structures.
171
+
172
+ ```typescript
173
+ import { CircularTracker } from 'canonize';
174
+
175
+ const tracker = new CircularTracker();
176
+ const obj = { self: null };
177
+ obj.self = obj; // circular reference
178
+
179
+ tracker.has(obj); // false
180
+ tracker.add(obj);
181
+ tracker.has(obj); // true
182
+ ```
183
+
184
+ ---
185
+
186
+ ### Schema Creation Helpers
187
+
188
+ #### `createCanonizePrimitive(primitive: CanonizePrimitive): ZodTypeAny`
189
+
190
+ Creates a coerced Zod schema for a primitive type.
191
+
192
+ ```typescript
193
+ import { createCanonizePrimitive } from 'canonize';
194
+
195
+ const stringSchema = createCanonizePrimitive('string');
196
+ const numberSchema = createCanonizePrimitive('number');
197
+ const booleanSchema = createCanonizePrimitive('boolean');
198
+ const nullSchema = createCanonizePrimitive('null');
199
+ ```
200
+
201
+ **Supported primitives:** `'string' | 'number' | 'boolean' | 'null'`
202
+
203
+ #### `createCanonizeSchema<T>(schema: T): ZodObject`
204
+
205
+ Creates a Zod object schema from a record of primitive type names.
206
+
207
+ ```typescript
208
+ import { createCanonizeSchema } from 'canonize';
209
+
210
+ const schema = createCanonizeSchema({
211
+ name: 'string',
212
+ age: 'number',
213
+ active: 'boolean',
214
+ });
215
+
216
+ schema.parse({ name: 123, age: '30', active: 'yes' });
217
+ // { name: '123', age: 30, active: true }
218
+ ```
219
+
220
+ ---
221
+
222
+ ### Constants
223
+
224
+ #### `ZodType`
225
+
226
+ Object containing Zod type name constants for use in type detection.
227
+
228
+ ```typescript
229
+ import { ZodType } from 'canonize';
230
+
231
+ ZodType.STRING; // 'string'
232
+ ZodType.NUMBER; // 'number'
233
+ ZodType.ARRAY; // 'array'
234
+ ZodType.OBJECT; // 'object'
235
+ ZodType.UNION; // 'union'
236
+ // ... and more
237
+ ```
238
+
239
+ **Available constants:**
240
+
241
+ | Category | Constants |
242
+ | ----------- | --------------------------------------------------------------------------- |
243
+ | Primitives | `STRING`, `NUMBER`, `BOOLEAN`, `DATE`, `BIGINT`, `NULL`, `UNDEFINED`, `NAN` |
244
+ | Collections | `ARRAY`, `OBJECT`, `TUPLE`, `RECORD`, `MAP`, `SET` |
245
+ | Composites | `UNION`, `DISCRIMINATED_UNION`, `INTERSECTION` |
246
+ | Enums | `ENUM`, `NATIVE_ENUM`, `LITERAL` |
247
+ | Wrappers | `OPTIONAL`, `NULLABLE`, `DEFAULT`, `CATCH`, `LAZY`, `READONLY`, `BRANDED` |
248
+ | Special | `ANY`, `UNKNOWN`, `NEVER`, `CUSTOM` |
249
+
250
+ ---
251
+
252
+ ### Types
253
+
254
+ #### `CanonizeSchema<T>`
255
+
256
+ Type alias representing a canonized schema. Preserves the original schema's type information.
257
+
258
+ ```typescript
259
+ import type { CanonizeSchema } from 'canonize';
260
+ import { z } from 'zod';
261
+
262
+ type MySchema = CanonizeSchema<z.ZodObject<{ name: z.ZodString }>>;
263
+ ```
264
+
265
+ #### `CanonizePrimitive`
266
+
267
+ Union type for primitive type names accepted by `createCanonizePrimitive`.
268
+
269
+ ```typescript
270
+ import type { CanonizePrimitive } from 'canonize';
271
+
272
+ const primitive: CanonizePrimitive = 'string'; // 'string' | 'number' | 'boolean' | 'null'
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Coercion Rules
278
+
279
+ ### String
280
+
281
+ | Input | Output |
282
+ | ------------------- | -------------- |
283
+ | `"hello"` | `"hello"` |
284
+ | `123` | `"123"` |
285
+ | `true` | `"true"` |
286
+ | `null`, `undefined` | `""` |
287
+ | `[1, 2, 3]` | `"1, 2, 3"` |
288
+ | `{ key: "value" }` | `"key: value"` |
289
+ | `new Date()` | ISO string |
290
+
291
+ ### Number
292
+
293
+ | Input | Output |
294
+ | -------------------- | --------- |
295
+ | `"42"` | `42` |
296
+ | `"42px"`, `"42em"` | `42` |
297
+ | `"1,234"`, `"1_234"` | `1234` |
298
+ | `"1e5"` | `100000` |
299
+ | `true` / `false` | `1` / `0` |
300
+ | `[42]` | `42` |
301
+
302
+ ### Boolean
303
+
304
+ | Input | Output |
305
+ | ------------------------------------------------------------- | ------- |
306
+ | `"true"`, `"yes"`, `"on"`, `"y"`, `"t"`, `"enabled"`, `"1"` | `true` |
307
+ | `"false"`, `"no"`, `"off"`, `"n"`, `"f"`, `"disabled"`, `"0"` | `false` |
308
+ | `1`, non-zero numbers | `true` |
309
+ | `0` | `false` |
310
+
311
+ ### Date
312
+
313
+ | Input | Output |
314
+ | ------------------- | ------------------ |
315
+ | ISO string | `new Date(string)` |
316
+ | Unix timestamp (ms) | `new Date(number)` |
317
+ | `"now"` | Current time |
318
+ | `"today"` | Start of today |
319
+ | `"yesterday"` | Start of yesterday |
320
+ | `"tomorrow"` | Start of tomorrow |
321
+
322
+ ### Array
323
+
324
+ | Input | Output |
325
+ | ------------------ | ----------------- |
326
+ | `"1,2,3"` | `["1", "2", "3"]` |
327
+ | `"[1,2,3]"` (JSON) | `[1, 2, 3]` |
328
+ | `null`, `""` | `[]` |
329
+ | `Set`, `Map` | Array from values |
330
+ | Single value | `[value]` |
331
+
332
+ ### Object
333
+
334
+ | Input | Output |
335
+ | ------------------- | ---------------------- |
336
+ | JSON string | Parsed object |
337
+ | `Map` | `Object.fromEntries()` |
338
+ | `null`, `undefined` | `{}` |
339
+
340
+ ### Union
341
+
342
+ Coercion tries options in order:
343
+
344
+ 1. Exact primitive match (preserves numbers in `string | number`)
345
+ 2. Object/record schemas for plain objects
346
+ 3. Array schemas for arrays and CSV strings
347
+ 4. Boolean schemas for boolean-like strings
348
+ 5. First union member, then remaining members
349
+
350
+ ### Discriminated Union
351
+
352
+ Uses the discriminator field to select the variant, then coerces fields:
353
+
354
+ ```typescript
355
+ const schema = canonize(
356
+ z.discriminatedUnion('type', [
357
+ z.object({ type: z.literal('a'), value: z.number() }),
358
+ z.object({ type: z.literal('b'), value: z.string() }),
359
+ ]),
360
+ );
361
+
362
+ schema.parse({ type: 'a', value: '42' }); // { type: 'a', value: 42 }
363
+ ```
364
+
365
+ ---
366
+
367
+ ## Tool Parameter Helpers
368
+
369
+ The `canonize/tool-parameters` module provides schema builders for LLM tool definitions. These handle malformed AI outputs gracefully with sensible defaults.
370
+
371
+ ```typescript
372
+ import {
373
+ boolean,
374
+ number,
375
+ string,
376
+ selector,
377
+ containerSelector,
378
+ collection,
379
+ numbers,
380
+ choices,
381
+ count,
382
+ url,
383
+ exportFormat,
384
+ imageFormat,
385
+ links,
386
+ linkMetadataSchema,
387
+ type LinkMetadata,
388
+ } from 'canonize/tool-parameters';
389
+ ```
390
+
391
+ ### `boolean(defaultValue)`
392
+
393
+ ```typescript
394
+ const enabled = boolean(true);
395
+ enabled.parse('yes'); // true
396
+ enabled.parse('FALSE'); // false
397
+ enabled.parse(1); // true
398
+ enabled.parse(undefined); // true (default)
399
+ ```
400
+
401
+ ### `number(defaultValue, options?)`
402
+
403
+ ```typescript
404
+ const count = number(10, { min: 1, max: 100, int: true });
405
+ count.parse('42px'); // 42
406
+ count.parse('1,234'); // 1234
407
+ count.parse(undefined); // 10 (default)
408
+ ```
409
+
410
+ ### `string()`
411
+
412
+ ```typescript
413
+ const name = string();
414
+ name.parse(' hello '); // 'hello' (trimmed)
415
+ name.parse(123); // '123'
416
+ ```
417
+
418
+ ### `selector()`
419
+
420
+ CSS selector string, trimmed and validated non-empty.
421
+
422
+ ```typescript
423
+ const sel = selector();
424
+ sel.parse(' .class '); // '.class'
425
+ ```
426
+
427
+ ### `containerSelector()`
428
+
429
+ Container selector with intelligent coercion for common LLM mistakes:
430
+
431
+ ```typescript
432
+ const container = containerSelector();
433
+ container.parse('main'); // 'main'
434
+ container.parse('*'); // null (wildcard → entire document)
435
+ container.parse('null'); // null
436
+ container.parse('a'); // null (link selector → entire document)
437
+ container.parse('body a'); // 'body' (extracts container)
438
+ container.parse('all'); // null (natural language)
439
+ ```
440
+
441
+ ### `collection(...defaultValues)`
442
+
443
+ String array with flexible separators (comma, semicolon, pipe, newline):
444
+
445
+ ```typescript
446
+ const tags = collection('default');
447
+ tags.parse('foo,bar'); // ['foo', 'bar']
448
+ tags.parse('foo;bar'); // ['foo', 'bar']
449
+ tags.parse('foo|bar'); // ['foo', 'bar']
450
+ tags.parse('foo\nbar'); // ['foo', 'bar']
451
+ tags.parse(undefined); // ['default']
452
+ ```
453
+
454
+ ### `numbers(options?)`
455
+
456
+ Number array with flexible input handling:
457
+
458
+ ```typescript
459
+ const ids = numbers({ int: true, min: 0 });
460
+ ids.parse('1,2,3'); // [1, 2, 3]
461
+ ids.parse([1, '2', 3]); // [1, 2, 3]
462
+ ids.parse(undefined); // []
463
+ ```
464
+
465
+ ### `choices(values, defaultValue?)`
466
+
467
+ Enum with fuzzy matching (case-insensitive, prefix, contains):
468
+
469
+ ```typescript
470
+ const sort = choices(['date', 'name', 'size'], 'date');
471
+ sort.parse('Date'); // 'date' (case-insensitive)
472
+ sort.parse('nam'); // 'name' (prefix match)
473
+ sort.parse('date_desc'); // 'date' (contains match)
474
+ ```
475
+
476
+ ### `count()`
477
+
478
+ Number for count/statistic values (defaults to 0):
479
+
480
+ ```typescript
481
+ const total = count();
482
+ total.parse('42'); // 42
483
+ total.parse(null); // 0
484
+ ```
485
+
486
+ ### `url()`
487
+
488
+ URL string with cleanup (removes wrapping quotes, brackets):
489
+
490
+ ```typescript
491
+ const link = url();
492
+ link.parse('"https://example.com"'); // 'https://example.com'
493
+ link.parse('<https://example.com>'); // 'https://example.com'
494
+ ```
495
+
496
+ ### `exportFormat(options?)`
497
+
498
+ Export format enum (`markdown`, `csv`, `json`):
499
+
500
+ ```typescript
501
+ exportFormat(); // defaults to 'markdown'
502
+ exportFormat({ defaultValue: 'csv' }); // defaults to 'csv'
503
+ exportFormat({ includeJson: false }); // 'markdown' | 'csv' only
504
+ ```
505
+
506
+ ### `imageFormat(defaultValue?)`
507
+
508
+ Image format enum (`jpeg`, `png`):
509
+
510
+ ```typescript
511
+ imageFormat(); // defaults to 'png'
512
+ imageFormat('jpeg'); // defaults to 'jpeg'
513
+ ```
514
+
515
+ ### `links()` and `linkMetadataSchema`
516
+
517
+ Array of link metadata objects:
518
+
519
+ ```typescript
520
+ const linkList = links();
521
+ linkList.parse([{ title: 'Example', url: 'https://example.com' }]);
522
+
523
+ // Or use the schema directly
524
+ import { linkMetadataSchema, type LinkMetadata } from 'canonize/tool-parameters';
525
+
526
+ const link: LinkMetadata = {
527
+ title: 'Example',
528
+ url: 'https://example.com',
529
+ source: 'html', // optional: 'html' | 'markdown' | 'element' | 'link'
530
+ rel: 'noopener', // optional
531
+ target: '_blank', // optional
532
+ referrerPolicy: null, // optional
533
+ text: 'Click here', // optional: raw link text
534
+ };
535
+ ```
536
+
537
+ ---
538
+
539
+ ## Advanced Usage
540
+
541
+ ### Lazy Schemas (Recursive Types)
542
+
543
+ ```typescript
544
+ const TreeNode = canonize(
545
+ z.lazy(() =>
546
+ z.object({
547
+ value: z.number(),
548
+ children: z.array(TreeNode).optional(),
549
+ }),
550
+ ),
551
+ );
552
+
553
+ TreeNode.parse({
554
+ value: '1',
555
+ children: [{ value: '2' }, { value: '3' }],
556
+ });
557
+ ```
558
+
559
+ ### Intersection Types
560
+
561
+ ```typescript
562
+ const schema = canonize(
563
+ z.intersection(z.object({ a: z.number() }), z.object({ b: z.string() })),
564
+ );
565
+
566
+ schema.parse({ a: '1', b: 2 }); // { a: 1, b: '2' }
567
+ ```
568
+
569
+ ### Map and Set
570
+
571
+ ```typescript
572
+ const mapSchema = canonize(z.map(z.string(), z.number()));
573
+ mapSchema.parse([
574
+ ['a', '1'],
575
+ ['b', '2'],
576
+ ]); // Map { 'a' => 1, 'b' => 2 }
577
+ mapSchema.parse({ a: '1', b: '2' }); // Map { 'a' => 1, 'b' => 2 }
578
+
579
+ const setSchema = canonize(z.set(z.number()));
580
+ setSchema.parse([1, '2', 3]); // Set { 1, 2, 3 }
581
+ setSchema.parse('1,2,3'); // Set { 1, 2, 3 }
582
+ ```
583
+
584
+ ---
585
+
586
+ ## Error Handling
587
+
588
+ Coercion errors are caught internally—the original value passes through to Zod for validation:
589
+
590
+ ```typescript
591
+ const schema = canonize(z.number());
592
+
593
+ schema.parse('42'); // 42 (coercion succeeds)
594
+ schema.parse('not a number'); // throws ZodError (coercion fails, Zod validates original)
595
+ ```
596
+
597
+ Circular references throw immediately:
598
+
599
+ ```typescript
600
+ const obj = { self: null };
601
+ obj.self = obj;
602
+
603
+ const schema = canonize(z.object({ self: z.any() }));
604
+ schema.parse(obj); // throws Error: Circular reference detected
605
+ ```
606
+
607
+ ---
608
+
609
+ ## StandardSchema Compatibility
610
+
611
+ Canonize is fully compatible with [StandardSchema](https://standardschema.dev/), the interoperability spec implemented by Zod, Valibot, ArkType, and others.
612
+
613
+ Since Zod v4 implements StandardSchema, all canonized schemas have the `~standard` property:
614
+
615
+ ```typescript
616
+ const schema = canonize(z.object({ count: z.number() }));
617
+
618
+ // Use with any StandardSchema-aware tool
619
+ const result = await schema['~standard'].validate({ count: '42' });
620
+ // { value: { count: 42 } }
621
+ ```
622
+
623
+ Canonize is Zod-specific because intelligent coercion requires schema introspection (knowing field types). StandardSchema only provides a `validate()` function without type information.
624
+
625
+ ---
626
+
627
+ ## License
628
+
629
+ MIT
@@ -0,0 +1,7 @@
1
+ import * as z from 'zod';
2
+ import { CircularTracker } from '../utilities/circular.js';
3
+ /**
4
+ * Coerce value to array with element type coercion
5
+ */
6
+ export declare function coerceToArray(value: unknown, _elementSchema: z.ZodTypeAny, coerceElement: (v: unknown, tracker: CircularTracker) => unknown, tracker?: CircularTracker): unknown[];
7
+ //# sourceMappingURL=array.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"array.d.ts","sourceRoot":"","sources":["../../src/coercers/array.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAEzB,OAAO,EAAiB,eAAe,EAAiB,MAAM,0BAA0B,CAAC;AAIzF;;GAEG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,EACd,cAAc,EAAE,CAAC,CAAC,UAAU,EAC5B,aAAa,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,KAAK,OAAO,EAChE,OAAO,kBAAwB,GAC9B,OAAO,EAAE,CA4FX"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Coerce any value to BigInt
3
+ */
4
+ export declare function coerceToBigInt(value: unknown): bigint;
5
+ //# sourceMappingURL=bigint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bigint.d.ts","sourceRoot":"","sources":["../../src/coercers/bigint.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAsErD"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Coerce any value to boolean
3
+ */
4
+ export declare function coerceToBoolean(value: unknown): boolean;
5
+ //# sourceMappingURL=boolean.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boolean.d.ts","sourceRoot":"","sources":["../../src/coercers/boolean.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAwDvD"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Coerce any value to Date
3
+ */
4
+ export declare function coerceToDate(value: unknown): Date;
5
+ //# sourceMappingURL=date.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"date.d.ts","sourceRoot":"","sources":["../../src/coercers/date.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CA4HjD"}
@@ -0,0 +1,7 @@
1
+ import * as z from 'zod';
2
+ import { CircularTracker } from '../utilities/circular.js';
3
+ /**
4
+ * Coerce value to discriminated union (intelligently matches based on discriminator)
5
+ */
6
+ export declare function coerceToDiscriminatedUnion(value: unknown, discriminatorKey: string, options: z.ZodTypeAny[], coerceOption: (index: number, v: unknown, tracker: CircularTracker) => unknown, tracker?: CircularTracker): unknown;
7
+ //# sourceMappingURL=discriminated-union.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discriminated-union.d.ts","sourceRoot":"","sources":["../../src/coercers/discriminated-union.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAEzB,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAK3D;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,OAAO,EACd,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,CAAC,CAAC,UAAU,EAAE,EACvB,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,KAAK,OAAO,EAC9E,OAAO,kBAAwB,GAC9B,OAAO,CAqFT"}