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.
- package/LICENSE +21 -0
- package/README.md +629 -0
- package/dist/coercers/array.d.ts +7 -0
- package/dist/coercers/array.d.ts.map +1 -0
- package/dist/coercers/bigint.d.ts +5 -0
- package/dist/coercers/bigint.d.ts.map +1 -0
- package/dist/coercers/boolean.d.ts +5 -0
- package/dist/coercers/boolean.d.ts.map +1 -0
- package/dist/coercers/date.d.ts +5 -0
- package/dist/coercers/date.d.ts.map +1 -0
- package/dist/coercers/discriminated-union.d.ts +7 -0
- package/dist/coercers/discriminated-union.d.ts.map +1 -0
- package/dist/coercers/enum.d.ts +9 -0
- package/dist/coercers/enum.d.ts.map +1 -0
- package/dist/coercers/intersection.d.ts +8 -0
- package/dist/coercers/intersection.d.ts.map +1 -0
- package/dist/coercers/literal.d.ts +5 -0
- package/dist/coercers/literal.d.ts.map +1 -0
- package/dist/coercers/map-set.d.ts +11 -0
- package/dist/coercers/map-set.d.ts.map +1 -0
- package/dist/coercers/nan.d.ts +5 -0
- package/dist/coercers/nan.d.ts.map +1 -0
- package/dist/coercers/null.d.ts +5 -0
- package/dist/coercers/null.d.ts.map +1 -0
- package/dist/coercers/number.d.ts +13 -0
- package/dist/coercers/number.d.ts.map +1 -0
- package/dist/coercers/object.d.ts +7 -0
- package/dist/coercers/object.d.ts.map +1 -0
- package/dist/coercers/record.d.ts +7 -0
- package/dist/coercers/record.d.ts.map +1 -0
- package/dist/coercers/string.d.ts +6 -0
- package/dist/coercers/string.d.ts.map +1 -0
- package/dist/coercers/tuple.d.ts +7 -0
- package/dist/coercers/tuple.d.ts.map +1 -0
- package/dist/coercers/union.d.ts +7 -0
- package/dist/coercers/union.d.ts.map +1 -0
- package/dist/constants.d.ts +36 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/create-schema.d.ts +5 -0
- package/dist/create-schema.d.ts.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1623 -0
- package/dist/index.js.map +33 -0
- package/dist/tool-parameters/index.d.ts +206 -0
- package/dist/tool-parameters/index.d.ts.map +1 -0
- package/dist/tool-parameters/index.js +301 -0
- package/dist/tool-parameters/index.js.map +12 -0
- package/dist/tool-parameters/link-metadata.d.ts +17 -0
- package/dist/tool-parameters/link-metadata.d.ts.map +1 -0
- package/dist/utilities/circular.d.ts +27 -0
- package/dist/utilities/circular.d.ts.map +1 -0
- package/dist/utilities/parsers.d.ts +29 -0
- package/dist/utilities/parsers.d.ts.map +1 -0
- package/dist/utilities/special-methods.d.ts +14 -0
- package/dist/utilities/special-methods.d.ts.map +1 -0
- package/dist/utilities/type-coercion.d.ts +11 -0
- package/dist/utilities/type-coercion.d.ts.map +1 -0
- package/dist/utilities/type-detection.d.ts +26 -0
- package/dist/utilities/type-detection.d.ts.map +1 -0
- package/dist/utilities/type-guards.d.ts +35 -0
- package/dist/utilities/type-guards.d.ts.map +1 -0
- 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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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"}
|