-
-
Notifications
You must be signed in to change notification settings - Fork 812
fix(core): Improves our schema to JSON Schema conversion (fix for zod 4) #2483
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"trigger.dev": patch | ||
--- | ||
|
||
Improves our schema to JSON Schema conversion, fixes zod 4 and a few other schema libraries, also correctly sets the dependencies | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,19 +42,19 @@ | |
}, | ||
"dependencies": { | ||
"@trigger.dev/core": "workspace:*", | ||
"zod-to-json-schema": "^3.24.5", | ||
"@sodaru/yup-to-json-schema": "^2.0.1" | ||
"zod-to-json-schema": "^3.24.0", | ||
"@sodaru/yup-to-json-schema": "^2", | ||
"zod": "3.25.76", | ||
"effect": "^3" | ||
}, | ||
Comment on lines
44
to
49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid hard runtime coupling to Zod/Effect; rely on peers + lazy import.
Proposed dependency adjustments (paired with the code changes suggested in "dependencies": {
"@trigger.dev/core": "workspace:*",
"zod-to-json-schema": "^3.24.0",
"@sodaru/yup-to-json-schema": "^2",
- "zod": "3.25.76",
- "effect": "^3"
+ // zod and effect are resolved at runtime if present (peer deps)
}, You already declare both in
🤖 Prompt for AI Agents
|
||
"devDependencies": { | ||
"arktype": "^2.0.0", | ||
"effect": "^3.11.11", | ||
"runtypes": "^6.7.0", | ||
"superstruct": "^2.0.2", | ||
"tshy": "^3.0.2", | ||
"@sinclair/typebox": "^0.34.3", | ||
"valibot": "^1.1.0", | ||
"yup": "^1.7.0", | ||
"zod": "^3.24.1 || ^4.0.0", | ||
"rimraf": "6.0.1", | ||
"@arethetypeswrong/cli": "^0.15.4" | ||
}, | ||
|
@@ -66,7 +66,7 @@ | |
"@sinclair/typebox": ">=0.34.30", | ||
"valibot": ">=0.41.0", | ||
"yup": ">=1.0.0", | ||
"zod": "^3.24.1 || ^4.0.0" | ||
"zod": "^3.25.76 || ^4" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Peer range looks good; conflict risk remains if we also list With 🤖 Prompt for AI Agents
|
||
}, | ||
"peerDependenciesMeta": { | ||
"arktype": { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,28 @@ | ||
// Import JSONSchema from core to ensure compatibility | ||
import type { JSONSchema } from "@trigger.dev/core/v3"; | ||
import { zodToJsonSchema } from "zod-to-json-schema"; | ||
import * as z4 from "zod/v4"; | ||
import { convertSchema } from "@sodaru/yup-to-json-schema"; | ||
import { JSONSchema as EffectJSONSchema } from "effect"; | ||
Comment on lines
+3
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Top-level imports make optional converters mandatory; This defeats the “only if available at runtime” goal and can break consumers lacking Zod v4. Make these lazy/optional. -import { zodToJsonSchema } from "zod-to-json-schema";
-import * as z4 from "zod/v4";
-import { convertSchema } from "@sodaru/yup-to-json-schema";
-import { JSONSchema as EffectJSONSchema } from "effect";
+import { createRequire } from "node:module";
+const requireOptional =
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
+ ((r => (id: string) => { try { return r(id); } catch { return undefined; } })
+ // @ts-expect-error: available in Node builds; CJS build will polyfill via tshy
+ )(typeof require !== "undefined" ? require : (await import("node:module")).createRequire(import.meta.url));
+
+let _z4: undefined | (typeof import("zod/v4"));
+const getZ4 = () => (_z4 ??= requireOptional("zod/v4"));
+
+let _zodToJsonSchema: undefined | (typeof import("zod-to-json-schema"))["zodToJsonSchema"];
+const getZodToJsonSchema = () => (_zodToJsonSchema ??= requireOptional("zod-to-json-schema")?.zodToJsonSchema);
+
+let _yupConvertSchema: undefined | (typeof import("@sodaru/yup-to-json-schema"))["convertSchema"];
+const getYupConvertSchema = () => (_yupConvertSchema ??= requireOptional("@sodaru/yup-to-json-schema")?.convertSchema);
+
+let _effectJSONSchemaMake: undefined | ((schema: unknown) => unknown);
+const getEffectJSONSchemaMake = () => (_effectJSONSchemaMake ??= requireOptional("effect")?.JSONSchema?.make); If you prefer, I can refactor to pure dynamic
🤖 Prompt for AI Agents
|
||
|
||
export type Schema = unknown; | ||
export type { JSONSchema }; | ||
|
||
export interface ConversionOptions { | ||
/** | ||
* The name to use for the schema in the JSON Schema | ||
* Enables support for references in the schema. | ||
* This is required for recursive schemas, e.g. with `z.lazy`. | ||
* However, not all language models and providers support such references. | ||
* Defaults to `false`. | ||
*/ | ||
name?: string; | ||
/** | ||
* Additional JSON Schema properties to merge | ||
*/ | ||
additionalProperties?: Record<string, unknown>; | ||
useReferences?: boolean; | ||
} | ||
|
||
export interface ConversionResult { | ||
/** | ||
* The JSON Schema representation (JSON Schema Draft 7) | ||
*/ | ||
jsonSchema: JSONSchema; | ||
/** | ||
* The detected schema type | ||
*/ | ||
schemaType: | ||
| "zod" | ||
| "yup" | ||
| "arktype" | ||
| "effect" | ||
| "valibot" | ||
| "superstruct" | ||
| "runtypes" | ||
| "typebox" | ||
| "unknown"; | ||
} | ||
|
||
/** | ||
|
@@ -57,107 +47,48 @@ export function schemaToJsonSchema( | |
if (typeof parser.toJsonSchema === "function") { | ||
try { | ||
const jsonSchema = parser.toJsonSchema(); | ||
// Determine if it's Zod or ArkType based on other methods | ||
const schemaType = | ||
typeof parser.parseAsync === "function" || typeof parser.parse === "function" | ||
? "zod" | ||
: "arktype"; | ||
|
||
return { | ||
jsonSchema: options?.additionalProperties | ||
? { ...jsonSchema, ...options.additionalProperties } | ||
: jsonSchema, | ||
schemaType, | ||
jsonSchema, | ||
}; | ||
} catch (error) { | ||
// If toJsonSchema fails, continue to other checks | ||
} | ||
} | ||
|
||
if (isZodSchema(parser)) { | ||
const jsonSchema = convertZodSchema(parser, options); | ||
|
||
if (jsonSchema) { | ||
return { | ||
jsonSchema: jsonSchema, | ||
}; | ||
} | ||
} | ||
|
||
// Check if it's a TypeBox schema (has Static and Kind symbols) | ||
if (parser[Symbol.for("TypeBox.Kind")] !== undefined) { | ||
// TypeBox schemas are already JSON Schema compliant | ||
return { | ||
jsonSchema: options?.additionalProperties | ||
? { ...parser, ...options.additionalProperties } | ||
: parser, | ||
schemaType: "typebox", | ||
jsonSchema: parser, | ||
}; | ||
} | ||
|
||
// For schemas that need external libraries, we need to check if they're available | ||
// This approach avoids bundling the dependencies while still allowing runtime usage | ||
|
||
// Check if it's a Zod schema (without built-in toJsonSchema) | ||
if (typeof parser.parseAsync === "function" || typeof parser.parse === "function") { | ||
try { | ||
// Try to access zod-to-json-schema if it's available | ||
// @ts-ignore - This is intentionally dynamic | ||
if (typeof globalThis.__zodToJsonSchema !== "undefined") { | ||
// @ts-ignore | ||
const { zodToJsonSchema } = globalThis.__zodToJsonSchema; | ||
const jsonSchema = options?.name | ||
? zodToJsonSchema(parser, options.name) | ||
: zodToJsonSchema(parser); | ||
|
||
if (jsonSchema && typeof jsonSchema === "object" && "$schema" in jsonSchema) { | ||
const { $schema, ...rest } = jsonSchema as any; | ||
return { | ||
jsonSchema: options?.additionalProperties | ||
? { ...rest, ...options.additionalProperties } | ||
: rest, | ||
schemaType: "zod", | ||
}; | ||
} | ||
|
||
return { | ||
jsonSchema: options?.additionalProperties | ||
? { ...jsonSchema, ...options.additionalProperties } | ||
: jsonSchema, | ||
schemaType: "zod", | ||
}; | ||
} | ||
} catch (error) { | ||
// Library not available | ||
} | ||
} | ||
|
||
// Check if it's a Yup schema | ||
if (typeof parser.validateSync === "function" && typeof parser.describe === "function") { | ||
try { | ||
// @ts-ignore | ||
if (typeof globalThis.__yupToJsonSchema !== "undefined") { | ||
// @ts-ignore | ||
const { convertSchema } = globalThis.__yupToJsonSchema; | ||
const jsonSchema = convertSchema(parser); | ||
return { | ||
jsonSchema: options?.additionalProperties | ||
? { ...jsonSchema, ...options.additionalProperties } | ||
: jsonSchema, | ||
schemaType: "yup", | ||
}; | ||
} | ||
} catch (error) { | ||
// Library not available | ||
if (isYupSchema(parser)) { | ||
const jsonSchema = convertYupSchema(parser); | ||
if (jsonSchema) { | ||
return { | ||
jsonSchema: jsonSchema, | ||
}; | ||
} | ||
} | ||
|
||
// Check if it's an Effect schema | ||
if (typeof parser.ast === "object" && typeof parser.ast._tag === "string") { | ||
try { | ||
// @ts-ignore | ||
if (typeof globalThis.__effectJsonSchema !== "undefined") { | ||
// @ts-ignore | ||
const { JSONSchema } = globalThis.__effectJsonSchema; | ||
const jsonSchema = JSONSchema.make(parser); | ||
return { | ||
jsonSchema: options?.additionalProperties | ||
? { ...jsonSchema, ...options.additionalProperties } | ||
: jsonSchema, | ||
schemaType: "effect", | ||
}; | ||
} | ||
} catch (error) { | ||
// Library not available | ||
if (isEffectSchema(parser)) { | ||
const jsonSchema = convertEffectSchema(parser); | ||
if (jsonSchema) { | ||
return { | ||
jsonSchema: jsonSchema, | ||
}; | ||
} | ||
} | ||
|
||
|
@@ -168,71 +99,75 @@ export function schemaToJsonSchema( | |
} | ||
|
||
/** | ||
* Initialize the schema conversion libraries | ||
* This should be called by the consuming application if they want to enable | ||
* conversion for schemas that don't have built-in JSON Schema support | ||
* Check if a schema can be converted to JSON Schema | ||
*/ | ||
export async function initializeSchemaConverters(): Promise<void> { | ||
try { | ||
// @ts-ignore | ||
globalThis.__zodToJsonSchema = await import("zod-to-json-schema"); | ||
} catch { | ||
// Zod conversion not available | ||
export function canConvertSchema(schema: Schema): boolean { | ||
const result = schemaToJsonSchema(schema); | ||
return result !== undefined; | ||
} | ||
|
||
export function isZodSchema(schema: any): boolean { | ||
return isZod3Schema(schema) || isZod4Schema(schema); | ||
} | ||
|
||
function isZod3Schema(schema: any): boolean { | ||
return "_def" in schema && "parse" in schema && "parseAsync" in schema && "safeParse" in schema; | ||
} | ||
|
||
function isZod4Schema(schema: any): boolean { | ||
return "_zod" in schema; | ||
} | ||
|
||
function convertZodSchema(schema: any, options?: ConversionOptions): JSONSchema | undefined { | ||
if (isZod4Schema(schema)) { | ||
return convertZod4Schema(schema, options); | ||
} | ||
|
||
try { | ||
// @ts-ignore | ||
globalThis.__yupToJsonSchema = await import("@sodaru/yup-to-json-schema"); | ||
} catch { | ||
// Yup conversion not available | ||
if (isZod3Schema(schema)) { | ||
return convertZod3Schema(schema, options); | ||
} | ||
|
||
try { | ||
// Try Effect first, then @effect/schema | ||
let module; | ||
try { | ||
module = await import("effect"); | ||
} catch {} | ||
return undefined; | ||
} | ||
|
||
if (module?.JSONSchema) { | ||
// @ts-ignore | ||
globalThis.__effectJsonSchema = { JSONSchema: module.JSONSchema }; | ||
} | ||
} catch { | ||
// Effect conversion not available | ||
} | ||
function convertZod3Schema(schema: any, options?: ConversionOptions): JSONSchema | undefined { | ||
const useReferences = options?.useReferences ?? false; | ||
|
||
return zodToJsonSchema(schema, { | ||
$refStrategy: useReferences ? "root" : "none", | ||
}) as JSONSchema; | ||
} | ||
Comment on lines
+133
to
139
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Guard Zod v3 converter behind optional import to avoid hard coupling. -function convertZod3Schema(schema: any, options?: ConversionOptions): JSONSchema | undefined {
- const useReferences = options?.useReferences ?? false;
- return zodToJsonSchema(schema, {
- $refStrategy: useReferences ? "root" : "none",
- }) as JSONSchema;
-}
+function convertZod3Schema(schema: any, options?: ConversionOptions): JSONSchema | undefined {
+ const zodToJsonSchema = getZodToJsonSchema();
+ if (!zodToJsonSchema) return undefined;
+ const useReferences = options?.useReferences ?? false;
+ return zodToJsonSchema(schema, { $refStrategy: useReferences ? "root" : "none" }) as JSONSchema;
+}
|
||
|
||
/** | ||
* Check if a schema can be converted to JSON Schema | ||
*/ | ||
export function canConvertSchema(schema: Schema): boolean { | ||
const result = schemaToJsonSchema(schema); | ||
return result !== undefined; | ||
function convertZod4Schema(schema: any, options?: ConversionOptions): JSONSchema | undefined { | ||
const useReferences = options?.useReferences ?? false; | ||
|
||
return z4.toJSONSchema(schema, { | ||
target: "draft-7", | ||
io: "output", | ||
reused: useReferences ? "ref" : "inline", | ||
}) as JSONSchema; | ||
} | ||
Comment on lines
+141
to
149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Guard Zod v4 path behind optional import. Avoid requiring Zod v4 in environments with only Zod v3 installed. -function convertZod4Schema(schema: any, options?: ConversionOptions): JSONSchema | undefined {
- const useReferences = options?.useReferences ?? false;
- return z4.toJSONSchema(schema, {
+function convertZod4Schema(schema: any, options?: ConversionOptions): JSONSchema | undefined {
+ const z4 = getZ4();
+ if (!z4?.toJSONSchema) return undefined;
+ const useReferences = options?.useReferences ?? false;
+ return z4.toJSONSchema(schema, {
target: "draft-7",
io: "output",
reused: useReferences ? "ref" : "inline",
}) as JSONSchema;
}
|
||
|
||
/** | ||
* Get the detected schema type | ||
*/ | ||
export function detectSchemaType(schema: Schema): ConversionResult["schemaType"] { | ||
const result = schemaToJsonSchema(schema); | ||
return result?.schemaType ?? "unknown"; | ||
function isYupSchema(schema: any): boolean { | ||
return "spec" in schema && "_typeCheck" in schema; | ||
} | ||
|
||
/** | ||
* Check if the conversion libraries are initialized | ||
*/ | ||
export function areConvertersInitialized(): { | ||
zod: boolean; | ||
yup: boolean; | ||
effect: boolean; | ||
} { | ||
return { | ||
// @ts-ignore | ||
zod: typeof globalThis.__zodToJsonSchema !== "undefined", | ||
// @ts-ignore | ||
yup: typeof globalThis.__yupToJsonSchema !== "undefined", | ||
// @ts-ignore | ||
effect: typeof globalThis.__effectJsonSchema !== "undefined", | ||
}; | ||
function convertYupSchema(schema: any): JSONSchema | undefined { | ||
try { | ||
return convertSchema(schema) as JSONSchema; | ||
} catch { | ||
return undefined; | ||
} | ||
} | ||
|
||
function isEffectSchema(schema: any): boolean { | ||
return "ast" in schema && typeof schema.ast === "object" && typeof schema.ast._tag === "string"; | ||
} | ||
|
||
function convertEffectSchema(schema: any): JSONSchema | undefined { | ||
try { | ||
return EffectJSONSchema.make(schema) as JSONSchema; | ||
} catch { | ||
return undefined; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Polish the release note (grammar and capitalization).
Capitalize “Zod” and tighten the sentence.
📝 Committable suggestion
🤖 Prompt for AI Agents