Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/angry-files-yawn.md
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Polish the release note (grammar and capitalization).

Capitalize “Zod” and tighten the sentence.

-Improves our schema to JSON Schema conversion, fixes zod 4 and a few other schema libraries, also correctly sets the dependencies
+Improve schema-to-JSON Schema conversion, fix Zod v4 compatibility and a few other schema libraries, and correct dependency setup.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Improves our schema to JSON Schema conversion, fixes zod 4 and a few other schema libraries, also correctly sets the dependencies
Improve schema-to-JSON Schema conversion, fix Zod v4 compatibility and a few other schema libraries, and correct dependency setup.
🤖 Prompt for AI Agents
In .changeset/angry-files-yawn.md around line 5, the release note sentence needs
capitalization and tightening: change “zod 4” to “Zod 4” and make the sentence
more concise and parallel (for example: “Improves our schema-to-JSON-Schema
conversion, fixes Zod 4 and a few other schema libraries, and correctly sets the
dependencies”). Update the single line accordingly to match grammar and
capitalization.

4 changes: 1 addition & 3 deletions packages/cli-v3/src/entryPoints/dev-index-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { registerResources } from "../indexing/registerResources.js";
import { env } from "std-env";
import { normalizeImportPath } from "../utilities/normalizeImportPath.js";
import { detectRuntimeVersion } from "@trigger.dev/core/v3/build";
import { schemaToJsonSchema, initializeSchemaConverters } from "@trigger.dev/schema-to-json";
import { schemaToJsonSchema } from "@trigger.dev/schema-to-json";

sourceMapSupport.install({
handleUncaughtExceptions: false,
Expand Down Expand Up @@ -193,8 +193,6 @@ await new Promise<void>((resolve) => {
});

async function convertSchemasToJsonSchemas(tasks: TaskManifest[]): Promise<TaskManifest[]> {
await initializeSchemaConverters();

const convertedTasks = tasks.map((task) => {
const schema = resourceCatalog.getTaskSchema(task.id);

Expand Down
4 changes: 1 addition & 3 deletions packages/cli-v3/src/entryPoints/managed-index-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { registerResources } from "../indexing/registerResources.js";
import { env } from "std-env";
import { normalizeImportPath } from "../utilities/normalizeImportPath.js";
import { detectRuntimeVersion } from "@trigger.dev/core/v3/build";
import { schemaToJsonSchema, initializeSchemaConverters } from "@trigger.dev/schema-to-json";
import { schemaToJsonSchema } from "@trigger.dev/schema-to-json";

sourceMapSupport.install({
handleUncaughtExceptions: false,
Expand Down Expand Up @@ -201,8 +201,6 @@ await new Promise<void>((resolve) => {
});

async function convertSchemasToJsonSchemas(tasks: TaskManifest[]): Promise<TaskManifest[]> {
await initializeSchemaConverters();

const convertedTasks = tasks.map((task) => {
const schema = resourceCatalog.getTaskSchema(task.id);

Expand Down
10 changes: 5 additions & 5 deletions packages/schema-to-json/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Avoid hard runtime coupling to Zod/Effect; rely on peers + lazy import.

src/index.ts currently imports zod/v4 and effect at top-level. Keeping them as direct deps forces consumers to install/bundle them even if unused and causes conflicts (notably zod/v4 when only Zod v3 is present). Prefer optional peer deps + lazy loading in code.

Proposed dependency adjustments (paired with the code changes suggested in src/index.ts):

   "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 peerDependencies (optional). After we lazy-load them in code, this will eliminate duplicate/forced installs. I can send a follow-up PR diff to keep commas/formatting correct across the JSON.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/schema-to-json/package.json around lines 44 to 49, the package
currently hard-declares zod and effect as runtime dependencies which forces
consumers to install/bundle specific versions; remove zod and effect from
"dependencies" (leave them as optional peerDependencies), then update
src/index.ts to lazy-load them via dynamic import (e.g., await import('zod') /
await import('effect') or require at call-time), add clear runtime checks that
throw informative errors if a needed peer is missing, and ensure the code paths
that don’t need Zod/Effect never import them so consumers who don’t use those
features aren’t forced to install those packages.

"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"
},
Expand All @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The 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 zod as a direct dep.

With "zod": "^3.25.76 || ^4" as a peer, drop the direct dependency to prevent dual-installs and resolution of zod/v4 failing when only v3 is present.

🤖 Prompt for AI Agents
In packages/schema-to-json/package.json around line 69, the package declares
"zod" as a peer dependency ("^3.25.76 || ^4") but still lists zod as a direct
dependency which can cause duplicate installs and v4 resolution failures when
only v3 is present; remove zod from the direct "dependencies" (or
"devDependencies" if present) so it is only a peerDependency, update
package.json accordingly, and run a quick install/test to confirm no code paths
relied on the direct import (if any test/build scripts require zod during
development, instead add it to devDependencies in the consuming workspace only).

},
"peerDependenciesMeta": {
"arktype": {
Expand Down
251 changes: 93 additions & 158 deletions packages/schema-to-json/src/index.ts
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Top-level imports make optional converters mandatory; zod/v4 will crash when only Zod v3 is installed.

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 import() with small async wrappers and keep the public API sync by returning undefined when modules aren’t present.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/schema-to-json/src/index.ts around lines 3-6, avoid top-level
imports of optional converters (e.g., "zod/v4") which crash when those versions
are absent; change to lazy runtime resolution by replacing the direct imports
with dynamic imports inside the functions that need them (use try/catch around
import() and return undefined or fallback behavior if the module is not
present), ensure any exported conversion functions gracefully handle a missing
module by returning undefined or an error value synchronously, and update
typings to use broad types or conditional types so the module compiles without
the optional dependency.


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";
}

/**
Expand All @@ -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,
};
}
}

Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
+}

Committable suggestion skipped: line range outside the PR's diff.


/**
* 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
 }

Committable suggestion skipped: line range outside the PR's diff.


/**
* 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;
}
}
Loading