zodvex 0.2.4 → 0.3.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 +319 -7
- package/dist/index.d.ts +159 -66
- package/dist/index.js +273 -209
- package/dist/index.js.map +1 -1
- package/package.json +13 -24
- package/src/__type-tests__/infer-returns.ts +154 -0
- package/src/custom.ts +15 -6
- package/src/ids.ts +8 -8
- package/src/registry.ts +171 -0
- package/src/tables.ts +173 -30
- package/src/types.ts +10 -22
- package/src/wrappers.ts +2 -2
- package/dist/index.d.mts +0 -489
- package/dist/index.mjs +0 -1001
- package/dist/index.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zodvex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Zod <=> Convex integration, supporting Zod 4",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"zod",
|
|
7
|
-
"convex",
|
|
8
|
-
"validators",
|
|
9
|
-
"codec",
|
|
10
|
-
"mapping",
|
|
11
|
-
"schema",
|
|
12
|
-
"validation"
|
|
13
|
-
],
|
|
5
|
+
"keywords": ["zod", "convex", "validators", "codec", "mapping", "schema", "validation"],
|
|
14
6
|
"homepage": "https://github.com/panzacoder/zodvex#readme",
|
|
15
7
|
"bugs": {
|
|
16
8
|
"url": "https://github.com/panzacoder/zodvex/issues"
|
|
@@ -21,24 +13,22 @@
|
|
|
21
13
|
},
|
|
22
14
|
"license": "MIT",
|
|
23
15
|
"author": "Jake Hebert",
|
|
16
|
+
"type": "module",
|
|
24
17
|
"main": "./dist/index.js",
|
|
25
|
-
"module": "./dist/index.js",
|
|
26
18
|
"types": "./dist/index.d.ts",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": ["dist", "src", "README.md", "LICENSE"],
|
|
33
27
|
"scripts": {
|
|
34
28
|
"build": "tsup",
|
|
35
29
|
"dev": "bun run tsup --watch",
|
|
36
30
|
"type-check": "bun run tsc --noEmit",
|
|
37
31
|
"test": "bun test",
|
|
38
|
-
"test:vitest": "bun run vitest",
|
|
39
|
-
"test:types": "bun run vitest --typecheck.only",
|
|
40
|
-
"test:all": "bun test && bun run test:types",
|
|
41
|
-
"test:coverage": "bun run vitest run --coverage",
|
|
42
32
|
"lint": "bun x biome check src __tests__",
|
|
43
33
|
"lint:fix": "bun x biome check --write src __tests__",
|
|
44
34
|
"format": "bun x biome format --write src __tests__",
|
|
@@ -54,10 +44,9 @@
|
|
|
54
44
|
"@biomejs/biome": "^2.2.6",
|
|
55
45
|
"@types/bun": "^1.3.0",
|
|
56
46
|
"@types/node": "^24.8.0",
|
|
57
|
-
"
|
|
47
|
+
"ai": "^6.0.6",
|
|
58
48
|
"tsup": "^8.5.0",
|
|
59
|
-
"typescript": "^5.9.3"
|
|
60
|
-
"vitest": "^3.2.4"
|
|
49
|
+
"typescript": "^5.9.3"
|
|
61
50
|
},
|
|
62
51
|
"engines": {
|
|
63
52
|
"node": ">=20",
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time type tests for InferReturns (Issue #19)
|
|
3
|
+
*
|
|
4
|
+
* These assertions cause TypeScript errors if types don't match expectations.
|
|
5
|
+
* This file is type-checked but not included in the bundle.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
import type { InferReturns } from '../types'
|
|
9
|
+
|
|
10
|
+
// Test helper: causes TS error if assigned `any`
|
|
11
|
+
declare function expectNotAny<T>(value: 0 extends 1 & T ? never : T): void
|
|
12
|
+
|
|
13
|
+
// --- Simple schemas should infer correctly ---
|
|
14
|
+
|
|
15
|
+
declare const stringResult: InferReturns<z.ZodString>
|
|
16
|
+
expectNotAny(stringResult)
|
|
17
|
+
|
|
18
|
+
declare const objectResult: InferReturns<ReturnType<typeof z.object<{ name: z.ZodString }>>>
|
|
19
|
+
expectNotAny(objectResult)
|
|
20
|
+
|
|
21
|
+
// --- Union schemas: this is the Issue #19 bug ---
|
|
22
|
+
// If InferReturns bails to `any` for unions, these will fail
|
|
23
|
+
|
|
24
|
+
declare const unionSchema: z.ZodUnion<[z.ZodString, z.ZodNumber]>
|
|
25
|
+
declare const unionResult: InferReturns<typeof unionSchema>
|
|
26
|
+
// @ts-expect-error - If this errors with "unused directive", Result is `any` (the bug)
|
|
27
|
+
const _unionInvalid: typeof unionResult = { notStringOrNumber: true }
|
|
28
|
+
|
|
29
|
+
declare const literalUnionSchema: z.ZodUnion<[z.ZodLiteral<'a'>, z.ZodLiteral<'b'>]>
|
|
30
|
+
declare const literalUnionResult: InferReturns<typeof literalUnionSchema>
|
|
31
|
+
// @ts-expect-error - If this errors with "unused directive", Result is `any` (the bug)
|
|
32
|
+
const _literalInvalid: typeof literalUnionResult = { notAOrB: true }
|
|
33
|
+
|
|
34
|
+
// --- Complex/deeply nested schemas (stress tests for TS depth limits) ---
|
|
35
|
+
// These tests ensure removing bailouts doesn't cause "Type instantiation is
|
|
36
|
+
// excessively deep and possibly infinite" errors.
|
|
37
|
+
|
|
38
|
+
// Deeply nested object (5 levels)
|
|
39
|
+
const deeplyNestedSchema = z.object({
|
|
40
|
+
level1: z.object({
|
|
41
|
+
level2: z.object({
|
|
42
|
+
level3: z.object({
|
|
43
|
+
level4: z.object({
|
|
44
|
+
level5: z.object({
|
|
45
|
+
value: z.string()
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
declare const deeplyNestedResult: InferReturns<typeof deeplyNestedSchema>
|
|
53
|
+
expectNotAny(deeplyNestedResult)
|
|
54
|
+
|
|
55
|
+
// Large discriminated union (10 variants) - common in real apps
|
|
56
|
+
const largeDiscriminatedUnion = z.discriminatedUnion('type', [
|
|
57
|
+
z.object({ type: z.literal('variant1'), data: z.string() }),
|
|
58
|
+
z.object({ type: z.literal('variant2'), count: z.number() }),
|
|
59
|
+
z.object({ type: z.literal('variant3'), flag: z.boolean() }),
|
|
60
|
+
z.object({ type: z.literal('variant4'), items: z.array(z.string()) }),
|
|
61
|
+
z.object({ type: z.literal('variant5'), nested: z.object({ a: z.number() }) }),
|
|
62
|
+
z.object({ type: z.literal('variant6'), opt: z.string().optional() }),
|
|
63
|
+
z.object({ type: z.literal('variant7'), nullable: z.string().nullable() }),
|
|
64
|
+
z.object({ type: z.literal('variant8'), tuple: z.tuple([z.string(), z.number()]) }),
|
|
65
|
+
z.object({ type: z.literal('variant9'), record: z.record(z.string(), z.number()) }),
|
|
66
|
+
z.object({ type: z.literal('variant10'), union: z.union([z.string(), z.number()]) })
|
|
67
|
+
])
|
|
68
|
+
declare const largeUnionResult: InferReturns<typeof largeDiscriminatedUnion>
|
|
69
|
+
expectNotAny(largeUnionResult)
|
|
70
|
+
|
|
71
|
+
// Nested unions (union containing objects with union fields)
|
|
72
|
+
const nestedUnionSchema = z.object({
|
|
73
|
+
outer: z.union([
|
|
74
|
+
z.object({
|
|
75
|
+
kind: z.literal('a'),
|
|
76
|
+
inner: z.union([z.literal('x'), z.literal('y')])
|
|
77
|
+
}),
|
|
78
|
+
z.object({
|
|
79
|
+
kind: z.literal('b'),
|
|
80
|
+
inner: z.union([z.literal('p'), z.literal('q')])
|
|
81
|
+
})
|
|
82
|
+
])
|
|
83
|
+
})
|
|
84
|
+
declare const nestedUnionResult: InferReturns<typeof nestedUnionSchema>
|
|
85
|
+
expectNotAny(nestedUnionResult)
|
|
86
|
+
|
|
87
|
+
// Array of unions
|
|
88
|
+
const arrayOfUnionsSchema = z.array(
|
|
89
|
+
z.union([
|
|
90
|
+
z.object({ type: z.literal('item'), value: z.string() }),
|
|
91
|
+
z.object({ type: z.literal('group'), children: z.array(z.string()) })
|
|
92
|
+
])
|
|
93
|
+
)
|
|
94
|
+
declare const arrayOfUnionsResult: InferReturns<typeof arrayOfUnionsSchema>
|
|
95
|
+
expectNotAny(arrayOfUnionsResult)
|
|
96
|
+
|
|
97
|
+
// Real-world pattern: API response with polymorphic data
|
|
98
|
+
const apiResponseSchema = z.object({
|
|
99
|
+
success: z.boolean(),
|
|
100
|
+
data: z
|
|
101
|
+
.union([
|
|
102
|
+
z.object({
|
|
103
|
+
type: z.literal('user'),
|
|
104
|
+
id: z.string(),
|
|
105
|
+
name: z.string(),
|
|
106
|
+
email: z.string().email(),
|
|
107
|
+
roles: z.array(z.enum(['admin', 'user', 'guest']))
|
|
108
|
+
}),
|
|
109
|
+
z.object({
|
|
110
|
+
type: z.literal('organization'),
|
|
111
|
+
id: z.string(),
|
|
112
|
+
name: z.string(),
|
|
113
|
+
members: z.array(
|
|
114
|
+
z.object({
|
|
115
|
+
userId: z.string(),
|
|
116
|
+
role: z.enum(['owner', 'admin', 'member'])
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
}),
|
|
120
|
+
z.object({
|
|
121
|
+
type: z.literal('error'),
|
|
122
|
+
code: z.number(),
|
|
123
|
+
message: z.string(),
|
|
124
|
+
details: z.record(z.string(), z.unknown()).optional()
|
|
125
|
+
})
|
|
126
|
+
])
|
|
127
|
+
.nullable(),
|
|
128
|
+
meta: z.object({
|
|
129
|
+
timestamp: z.number(),
|
|
130
|
+
requestId: z.string()
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
declare const apiResponseResult: InferReturns<typeof apiResponseSchema>
|
|
134
|
+
expectNotAny(apiResponseResult)
|
|
135
|
+
|
|
136
|
+
// Recursive-like pattern (not truly recursive, but deep)
|
|
137
|
+
const treeNodeSchema = z.object({
|
|
138
|
+
id: z.string(),
|
|
139
|
+
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
140
|
+
children: z.array(
|
|
141
|
+
z.object({
|
|
142
|
+
id: z.string(),
|
|
143
|
+
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
144
|
+
children: z.array(
|
|
145
|
+
z.object({
|
|
146
|
+
id: z.string(),
|
|
147
|
+
value: z.union([z.string(), z.number(), z.boolean()])
|
|
148
|
+
})
|
|
149
|
+
)
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
})
|
|
153
|
+
declare const treeNodeResult: InferReturns<typeof treeNodeSchema>
|
|
154
|
+
expectNotAny(treeNodeResult)
|
package/src/custom.ts
CHANGED
|
@@ -178,13 +178,15 @@ export function customFnBuilder<
|
|
|
178
178
|
|
|
179
179
|
return function customBuilder(fn: any): any {
|
|
180
180
|
const { args, handler = fn, returns: maybeObject, ...extra } = fn
|
|
181
|
+
const skipConvexValidation = fn.skipConvexValidation ?? false
|
|
181
182
|
|
|
182
183
|
const returns =
|
|
183
184
|
maybeObject && !(maybeObject instanceof z.ZodType) ? z.object(maybeObject) : maybeObject
|
|
185
|
+
// Only generate Convex return validator when not skipping Convex validation
|
|
184
186
|
const returnValidator =
|
|
185
|
-
returns && !
|
|
187
|
+
returns && !skipConvexValidation ? { returns: zodToConvex(returns) } : undefined
|
|
186
188
|
|
|
187
|
-
if (args
|
|
189
|
+
if (args) {
|
|
188
190
|
let argsValidator = args
|
|
189
191
|
let argsSchema: z.ZodObject<any>
|
|
190
192
|
|
|
@@ -202,9 +204,13 @@ export function customFnBuilder<
|
|
|
202
204
|
argsSchema = z.object(argsValidator)
|
|
203
205
|
}
|
|
204
206
|
|
|
205
|
-
|
|
207
|
+
// Only generate Convex args validator when not skipping Convex validation
|
|
208
|
+
const convexArgs = skipConvexValidation
|
|
209
|
+
? inputArgs
|
|
210
|
+
: { ...zodToConvexFields(argsValidator), ...inputArgs }
|
|
211
|
+
|
|
206
212
|
return builder({
|
|
207
|
-
args:
|
|
213
|
+
args: convexArgs,
|
|
208
214
|
...returnValidator,
|
|
209
215
|
handler: async (ctx: Ctx, allArgs: any) => {
|
|
210
216
|
const added: any = await customInput(
|
|
@@ -215,6 +221,7 @@ export function customFnBuilder<
|
|
|
215
221
|
const argKeys = Object.keys(argsValidator)
|
|
216
222
|
const rawArgs = pick(allArgs, argKeys)
|
|
217
223
|
const decoded = fromConvexJS(rawArgs, argsSchema)
|
|
224
|
+
// Always run Zod validation, regardless of skipConvexValidation
|
|
218
225
|
const parsed = argsSchema.safeParse(decoded)
|
|
219
226
|
if (!parsed.success) {
|
|
220
227
|
handleZodValidationError(parsed.error, 'args')
|
|
@@ -224,7 +231,8 @@ export function customFnBuilder<
|
|
|
224
231
|
const addedArgs = (added?.args as Record<string, unknown>) ?? {}
|
|
225
232
|
const finalArgs = { ...baseArgs, ...addedArgs }
|
|
226
233
|
const ret = await handler(finalCtx, finalArgs)
|
|
227
|
-
|
|
234
|
+
// Always run Zod return validation when returns schema is provided
|
|
235
|
+
if (returns) {
|
|
228
236
|
let validated: any
|
|
229
237
|
try {
|
|
230
238
|
validated = (returns as z.ZodTypeAny).parse(ret)
|
|
@@ -261,7 +269,8 @@ export function customFnBuilder<
|
|
|
261
269
|
const addedArgs = (added?.args as Record<string, unknown>) ?? {}
|
|
262
270
|
const finalArgs = { ...baseArgs, ...addedArgs }
|
|
263
271
|
const ret = await handler(finalCtx, finalArgs)
|
|
264
|
-
|
|
272
|
+
// Always run Zod return validation when returns schema is provided
|
|
273
|
+
if (returns) {
|
|
265
274
|
let validated: any
|
|
266
275
|
try {
|
|
267
276
|
validated = (returns as z.ZodTypeAny).parse(ret)
|
package/src/ids.ts
CHANGED
|
@@ -16,23 +16,21 @@ export const registryHelpers = {
|
|
|
16
16
|
/**
|
|
17
17
|
* Create a Zod validator for a Convex Id
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Compatible with AI SDK and other tools that don't support transforms.
|
|
20
|
+
* Uses type-level branding instead of runtime transforms for GenericId<T> compatibility.
|
|
21
|
+
*
|
|
22
|
+
* @param tableName - The Convex table name for this ID
|
|
23
|
+
* @returns A Zod string validator typed as GenericId<TableName>
|
|
21
24
|
*/
|
|
22
25
|
export function zid<TableName extends string>(
|
|
23
26
|
tableName: TableName
|
|
24
27
|
): z.ZodType<GenericId<TableName>> & { _tableName: TableName } {
|
|
25
|
-
//
|
|
28
|
+
// Create base string validator with refinement (no transform or brand)
|
|
26
29
|
const baseSchema = z
|
|
27
30
|
.string()
|
|
28
31
|
.refine(val => typeof val === 'string' && val.length > 0, {
|
|
29
32
|
message: `Invalid ID for table "${tableName}"`
|
|
30
33
|
})
|
|
31
|
-
.transform(val => {
|
|
32
|
-
// Cast to GenericId while keeping the string value
|
|
33
|
-
return val as string & GenericId<TableName>
|
|
34
|
-
})
|
|
35
|
-
.brand(`ConvexId_${tableName}`) // Use native Zod v4 .brand() method
|
|
36
34
|
// Add a human-readable marker for client-side introspection utilities
|
|
37
35
|
// used in apps/native (e.g., to detect relationship fields in dynamic forms).
|
|
38
36
|
.describe(`convexId:${tableName}`)
|
|
@@ -47,6 +45,8 @@ export function zid<TableName extends string>(
|
|
|
47
45
|
const branded = baseSchema as any
|
|
48
46
|
branded._tableName = tableName
|
|
49
47
|
|
|
48
|
+
// Type assertion provides GenericId<TableName> typing without runtime transform
|
|
49
|
+
// This maintains type safety while being compatible with AI SDK and similar tools
|
|
50
50
|
return branded as z.ZodType<GenericId<TableName>> & { _tableName: TableName }
|
|
51
51
|
}
|
|
52
52
|
|
package/src/registry.ts
CHANGED
|
@@ -56,3 +56,174 @@ export function isDateSchema(schema: any): boolean {
|
|
|
56
56
|
|
|
57
57
|
return false
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// JSON Schema Override Support
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Zod's toJSONSchema doesn't support transforms, brands, and other "unrepresentable"
|
|
64
|
+
// types by default. This module provides overrides for zodvex-managed types
|
|
65
|
+
// so they can be used with AI SDKs and other JSON Schema-based tools.
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Checks if a schema is a zid (Convex ID) schema by looking at its description.
|
|
69
|
+
* zid schemas are marked with "convexId:{tableName}" in their description.
|
|
70
|
+
*/
|
|
71
|
+
export function isZidSchema(schema: z.ZodTypeAny): boolean {
|
|
72
|
+
const description = schema.description
|
|
73
|
+
return typeof description === 'string' && description.startsWith('convexId:')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extracts the table name from a zid schema's description.
|
|
78
|
+
* Returns undefined if not a zid schema.
|
|
79
|
+
*/
|
|
80
|
+
export function getZidTableName(schema: z.ZodTypeAny): string | undefined {
|
|
81
|
+
const description = schema.description
|
|
82
|
+
if (typeof description === 'string' && description.startsWith('convexId:')) {
|
|
83
|
+
return description.slice('convexId:'.length)
|
|
84
|
+
}
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Context object passed to the JSON Schema override function.
|
|
90
|
+
* Uses 'any' types for compatibility with Zod's internal types.
|
|
91
|
+
*/
|
|
92
|
+
export interface JSONSchemaOverrideContext {
|
|
93
|
+
zodSchema: any // Zod's internal $ZodTypes
|
|
94
|
+
jsonSchema: any // Zod's JSONSchema.BaseSchema
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Override function for z.toJSONSchema that handles zodvex-managed types.
|
|
99
|
+
*
|
|
100
|
+
* Handles:
|
|
101
|
+
* - zid schemas: Converts to { type: "string" } with convexId format
|
|
102
|
+
* - z.date(): Converts to { type: "string", format: "date-time" }
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* import { z } from 'zod'
|
|
107
|
+
* import { zid, zodvexJSONSchemaOverride } from 'zodvex'
|
|
108
|
+
*
|
|
109
|
+
* const schema = z.object({
|
|
110
|
+
* userId: zid('users'),
|
|
111
|
+
* name: z.string()
|
|
112
|
+
* })
|
|
113
|
+
*
|
|
114
|
+
* const jsonSchema = z.toJSONSchema(schema, {
|
|
115
|
+
* unrepresentable: 'any',
|
|
116
|
+
* override: zodvexJSONSchemaOverride
|
|
117
|
+
* })
|
|
118
|
+
* // => { type: "object", properties: { userId: { type: "string" }, name: { type: "string" } } }
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function zodvexJSONSchemaOverride(ctx: JSONSchemaOverrideContext): void {
|
|
122
|
+
const { zodSchema, jsonSchema } = ctx
|
|
123
|
+
|
|
124
|
+
// Handle zid schemas (transforms with convexId description)
|
|
125
|
+
if (isZidSchema(zodSchema)) {
|
|
126
|
+
const tableName = getZidTableName(zodSchema)
|
|
127
|
+
// Set our properties - don't clear existing ones set by user overrides
|
|
128
|
+
// When unrepresentable: 'any', Zod already gives us {} so no clearing needed
|
|
129
|
+
jsonSchema.type = 'string'
|
|
130
|
+
if (tableName) {
|
|
131
|
+
jsonSchema.format = `convex-id:${tableName}`
|
|
132
|
+
}
|
|
133
|
+
// Preserve the description from .describe() - this is what the LLM sees
|
|
134
|
+
if (zodSchema.description) {
|
|
135
|
+
jsonSchema.description = zodSchema.description
|
|
136
|
+
}
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Handle z.date() - convert to ISO 8601 string format
|
|
141
|
+
// Zod v4 passes real schema instances here (ZodDate has `type === 'date'`).
|
|
142
|
+
if (zodSchema instanceof z.ZodDate || (zodSchema as any).type === 'date') {
|
|
143
|
+
jsonSchema.type = 'string'
|
|
144
|
+
jsonSchema.format = 'date-time'
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Composes multiple JSON Schema override functions into one.
|
|
151
|
+
* Overrides run in order - first override runs first.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* import { composeOverrides, zodvexJSONSchemaOverride } from 'zodvex'
|
|
156
|
+
*
|
|
157
|
+
* const myOverride = (ctx) => {
|
|
158
|
+
* if (ctx.zodSchema.description?.startsWith('myType:')) {
|
|
159
|
+
* ctx.jsonSchema.type = 'string'
|
|
160
|
+
* ctx.jsonSchema.format = 'my-format'
|
|
161
|
+
* }
|
|
162
|
+
* }
|
|
163
|
+
*
|
|
164
|
+
* // User's override runs first, then zodvex's
|
|
165
|
+
* z.toJSONSchema(schema, {
|
|
166
|
+
* unrepresentable: 'any',
|
|
167
|
+
* override: composeOverrides(myOverride, zodvexJSONSchemaOverride)
|
|
168
|
+
* })
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
export function composeOverrides(
|
|
172
|
+
...overrides: Array<((ctx: JSONSchemaOverrideContext) => void) | undefined>
|
|
173
|
+
): (ctx: JSONSchemaOverrideContext) => void {
|
|
174
|
+
return (ctx: JSONSchemaOverrideContext) => {
|
|
175
|
+
for (const override of overrides) {
|
|
176
|
+
override?.(ctx)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Options for toJSONSchema, matching Zod's interface.
|
|
183
|
+
*/
|
|
184
|
+
export interface ToJSONSchemaOptions {
|
|
185
|
+
target?: 'draft-4' | 'draft-7' | 'draft-2020-12' | 'openapi-3.0'
|
|
186
|
+
unrepresentable?: 'throw' | 'any'
|
|
187
|
+
cycles?: 'ref' | 'throw'
|
|
188
|
+
reused?: 'ref' | 'inline'
|
|
189
|
+
io?: 'input' | 'output'
|
|
190
|
+
override?: (ctx: JSONSchemaOverrideContext) => void
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Converts a Zod schema to JSON Schema with zodvex-aware overrides.
|
|
195
|
+
*
|
|
196
|
+
* This is a convenience wrapper around z.toJSONSchema that automatically
|
|
197
|
+
* handles zodvex-managed types like zid and dates.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```ts
|
|
201
|
+
* import { zid, toJSONSchema } from 'zodvex'
|
|
202
|
+
*
|
|
203
|
+
* const schema = z.object({
|
|
204
|
+
* userId: zid('users'),
|
|
205
|
+
* createdAt: z.date(),
|
|
206
|
+
* name: z.string()
|
|
207
|
+
* })
|
|
208
|
+
*
|
|
209
|
+
* const jsonSchema = toJSONSchema(schema)
|
|
210
|
+
* // Works with AI SDK's generateObject, etc.
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export function toJSONSchema<T extends z.ZodTypeAny>(
|
|
214
|
+
schema: T,
|
|
215
|
+
options?: ToJSONSchemaOptions
|
|
216
|
+
): Record<string, any> {
|
|
217
|
+
const userOverride = options?.override
|
|
218
|
+
|
|
219
|
+
return z.toJSONSchema(schema, {
|
|
220
|
+
...options,
|
|
221
|
+
// Default to 'any' so transforms don't throw
|
|
222
|
+
unrepresentable: options?.unrepresentable ?? 'any',
|
|
223
|
+
// Chain our override with user's override
|
|
224
|
+
override: ctx => {
|
|
225
|
+
zodvexJSONSchemaOverride(ctx)
|
|
226
|
+
userOverride?.(ctx)
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
}
|