Skip to content

Commit 92cb7a3

Browse files
committed
fix(core): Improves our schema to JSON Schema conversion (fix for zod 4)
1 parent 5db583b commit 92cb7a3

File tree

8 files changed

+215
-329
lines changed

8 files changed

+215
-329
lines changed

.changeset/angry-files-yawn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
Improves our schema to JSON Schema conversion, fixes zod 4 and a few other schema libraries, also correctly sets the dependencies

packages/cli-v3/src/entryPoints/dev-index-worker.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { registerResources } from "../indexing/registerResources.js";
1818
import { env } from "std-env";
1919
import { normalizeImportPath } from "../utilities/normalizeImportPath.js";
2020
import { detectRuntimeVersion } from "@trigger.dev/core/v3/build";
21-
import { schemaToJsonSchema, initializeSchemaConverters } from "@trigger.dev/schema-to-json";
21+
import { schemaToJsonSchema } from "@trigger.dev/schema-to-json";
2222

2323
sourceMapSupport.install({
2424
handleUncaughtExceptions: false,
@@ -193,8 +193,6 @@ await new Promise<void>((resolve) => {
193193
});
194194

195195
async function convertSchemasToJsonSchemas(tasks: TaskManifest[]): Promise<TaskManifest[]> {
196-
await initializeSchemaConverters();
197-
198196
const convertedTasks = tasks.map((task) => {
199197
const schema = resourceCatalog.getTaskSchema(task.id);
200198

packages/cli-v3/src/entryPoints/managed-index-worker.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { registerResources } from "../indexing/registerResources.js";
1818
import { env } from "std-env";
1919
import { normalizeImportPath } from "../utilities/normalizeImportPath.js";
2020
import { detectRuntimeVersion } from "@trigger.dev/core/v3/build";
21-
import { schemaToJsonSchema, initializeSchemaConverters } from "@trigger.dev/schema-to-json";
21+
import { schemaToJsonSchema } from "@trigger.dev/schema-to-json";
2222

2323
sourceMapSupport.install({
2424
handleUncaughtExceptions: false,
@@ -201,8 +201,6 @@ await new Promise<void>((resolve) => {
201201
});
202202

203203
async function convertSchemasToJsonSchemas(tasks: TaskManifest[]): Promise<TaskManifest[]> {
204-
await initializeSchemaConverters();
205-
206204
const convertedTasks = tasks.map((task) => {
207205
const schema = resourceCatalog.getTaskSchema(task.id);
208206

packages/schema-to-json/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,19 @@
4242
},
4343
"dependencies": {
4444
"@trigger.dev/core": "workspace:*",
45-
"zod-to-json-schema": "^3.24.5",
46-
"@sodaru/yup-to-json-schema": "^2.0.1"
45+
"zod-to-json-schema": "^3.24.0",
46+
"@sodaru/yup-to-json-schema": "^2",
47+
"zod": "3.25.76",
48+
"effect": "^3"
4749
},
4850
"devDependencies": {
4951
"arktype": "^2.0.0",
50-
"effect": "^3.11.11",
5152
"runtypes": "^6.7.0",
5253
"superstruct": "^2.0.2",
5354
"tshy": "^3.0.2",
5455
"@sinclair/typebox": "^0.34.3",
5556
"valibot": "^1.1.0",
5657
"yup": "^1.7.0",
57-
"zod": "^3.24.1 || ^4.0.0",
5858
"rimraf": "6.0.1",
5959
"@arethetypeswrong/cli": "^0.15.4"
6060
},
@@ -66,7 +66,7 @@
6666
"@sinclair/typebox": ">=0.34.30",
6767
"valibot": ">=0.41.0",
6868
"yup": ">=1.0.0",
69-
"zod": "^3.24.1 || ^4.0.0"
69+
"zod": "^3.25.76 || ^4"
7070
},
7171
"peerDependenciesMeta": {
7272
"arktype": {

packages/schema-to-json/src/index.ts

Lines changed: 93 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,28 @@
11
// Import JSONSchema from core to ensure compatibility
22
import type { JSONSchema } from "@trigger.dev/core/v3";
3+
import { zodToJsonSchema } from "zod-to-json-schema";
4+
import * as z4 from "zod/v4";
5+
import { convertSchema } from "@sodaru/yup-to-json-schema";
6+
import { JSONSchema as EffectJSONSchema } from "effect";
37

48
export type Schema = unknown;
59
export type { JSONSchema };
610

711
export interface ConversionOptions {
812
/**
9-
* The name to use for the schema in the JSON Schema
13+
* Enables support for references in the schema.
14+
* This is required for recursive schemas, e.g. with `z.lazy`.
15+
* However, not all language models and providers support such references.
16+
* Defaults to `false`.
1017
*/
11-
name?: string;
12-
/**
13-
* Additional JSON Schema properties to merge
14-
*/
15-
additionalProperties?: Record<string, unknown>;
18+
useReferences?: boolean;
1619
}
1720

1821
export interface ConversionResult {
1922
/**
2023
* The JSON Schema representation (JSON Schema Draft 7)
2124
*/
2225
jsonSchema: JSONSchema;
23-
/**
24-
* The detected schema type
25-
*/
26-
schemaType:
27-
| "zod"
28-
| "yup"
29-
| "arktype"
30-
| "effect"
31-
| "valibot"
32-
| "superstruct"
33-
| "runtypes"
34-
| "typebox"
35-
| "unknown";
3626
}
3727

3828
/**
@@ -57,107 +47,48 @@ export function schemaToJsonSchema(
5747
if (typeof parser.toJsonSchema === "function") {
5848
try {
5949
const jsonSchema = parser.toJsonSchema();
60-
// Determine if it's Zod or ArkType based on other methods
61-
const schemaType =
62-
typeof parser.parseAsync === "function" || typeof parser.parse === "function"
63-
? "zod"
64-
: "arktype";
50+
6551
return {
66-
jsonSchema: options?.additionalProperties
67-
? { ...jsonSchema, ...options.additionalProperties }
68-
: jsonSchema,
69-
schemaType,
52+
jsonSchema,
7053
};
7154
} catch (error) {
7255
// If toJsonSchema fails, continue to other checks
7356
}
7457
}
7558

59+
if (isZodSchema(parser)) {
60+
const jsonSchema = convertZodSchema(parser, options);
61+
62+
if (jsonSchema) {
63+
return {
64+
jsonSchema: jsonSchema,
65+
};
66+
}
67+
}
68+
7669
// Check if it's a TypeBox schema (has Static and Kind symbols)
7770
if (parser[Symbol.for("TypeBox.Kind")] !== undefined) {
7871
// TypeBox schemas are already JSON Schema compliant
7972
return {
80-
jsonSchema: options?.additionalProperties
81-
? { ...parser, ...options.additionalProperties }
82-
: parser,
83-
schemaType: "typebox",
73+
jsonSchema: parser,
8474
};
8575
}
8676

87-
// For schemas that need external libraries, we need to check if they're available
88-
// This approach avoids bundling the dependencies while still allowing runtime usage
89-
90-
// Check if it's a Zod schema (without built-in toJsonSchema)
91-
if (typeof parser.parseAsync === "function" || typeof parser.parse === "function") {
92-
try {
93-
// Try to access zod-to-json-schema if it's available
94-
// @ts-ignore - This is intentionally dynamic
95-
if (typeof globalThis.__zodToJsonSchema !== "undefined") {
96-
// @ts-ignore
97-
const { zodToJsonSchema } = globalThis.__zodToJsonSchema;
98-
const jsonSchema = options?.name
99-
? zodToJsonSchema(parser, options.name)
100-
: zodToJsonSchema(parser);
101-
102-
if (jsonSchema && typeof jsonSchema === "object" && "$schema" in jsonSchema) {
103-
const { $schema, ...rest } = jsonSchema as any;
104-
return {
105-
jsonSchema: options?.additionalProperties
106-
? { ...rest, ...options.additionalProperties }
107-
: rest,
108-
schemaType: "zod",
109-
};
110-
}
111-
112-
return {
113-
jsonSchema: options?.additionalProperties
114-
? { ...jsonSchema, ...options.additionalProperties }
115-
: jsonSchema,
116-
schemaType: "zod",
117-
};
118-
}
119-
} catch (error) {
120-
// Library not available
121-
}
122-
}
123-
124-
// Check if it's a Yup schema
125-
if (typeof parser.validateSync === "function" && typeof parser.describe === "function") {
126-
try {
127-
// @ts-ignore
128-
if (typeof globalThis.__yupToJsonSchema !== "undefined") {
129-
// @ts-ignore
130-
const { convertSchema } = globalThis.__yupToJsonSchema;
131-
const jsonSchema = convertSchema(parser);
132-
return {
133-
jsonSchema: options?.additionalProperties
134-
? { ...jsonSchema, ...options.additionalProperties }
135-
: jsonSchema,
136-
schemaType: "yup",
137-
};
138-
}
139-
} catch (error) {
140-
// Library not available
77+
if (isYupSchema(parser)) {
78+
const jsonSchema = convertYupSchema(parser);
79+
if (jsonSchema) {
80+
return {
81+
jsonSchema: jsonSchema,
82+
};
14183
}
14284
}
14385

144-
// Check if it's an Effect schema
145-
if (typeof parser.ast === "object" && typeof parser.ast._tag === "string") {
146-
try {
147-
// @ts-ignore
148-
if (typeof globalThis.__effectJsonSchema !== "undefined") {
149-
// @ts-ignore
150-
const { JSONSchema } = globalThis.__effectJsonSchema;
151-
const jsonSchema = JSONSchema.make(parser);
152-
return {
153-
jsonSchema: options?.additionalProperties
154-
? { ...jsonSchema, ...options.additionalProperties }
155-
: jsonSchema,
156-
schemaType: "effect",
157-
};
158-
}
159-
} catch (error) {
160-
// Library not available
86+
if (isEffectSchema(parser)) {
87+
const jsonSchema = convertEffectSchema(parser);
88+
if (jsonSchema) {
89+
return {
90+
jsonSchema: jsonSchema,
91+
};
16192
}
16293
}
16394

@@ -168,71 +99,75 @@ export function schemaToJsonSchema(
16899
}
169100

170101
/**
171-
* Initialize the schema conversion libraries
172-
* This should be called by the consuming application if they want to enable
173-
* conversion for schemas that don't have built-in JSON Schema support
102+
* Check if a schema can be converted to JSON Schema
174103
*/
175-
export async function initializeSchemaConverters(): Promise<void> {
176-
try {
177-
// @ts-ignore
178-
globalThis.__zodToJsonSchema = await import("zod-to-json-schema");
179-
} catch {
180-
// Zod conversion not available
104+
export function canConvertSchema(schema: Schema): boolean {
105+
const result = schemaToJsonSchema(schema);
106+
return result !== undefined;
107+
}
108+
109+
export function isZodSchema(schema: any): boolean {
110+
return isZod3Schema(schema) || isZod4Schema(schema);
111+
}
112+
113+
function isZod3Schema(schema: any): boolean {
114+
return "_def" in schema && "parse" in schema && "parseAsync" in schema && "safeParse" in schema;
115+
}
116+
117+
function isZod4Schema(schema: any): boolean {
118+
return "_zod" in schema;
119+
}
120+
121+
function convertZodSchema(schema: any, options?: ConversionOptions): JSONSchema | undefined {
122+
if (isZod4Schema(schema)) {
123+
return convertZod4Schema(schema, options);
181124
}
182125

183-
try {
184-
// @ts-ignore
185-
globalThis.__yupToJsonSchema = await import("@sodaru/yup-to-json-schema");
186-
} catch {
187-
// Yup conversion not available
126+
if (isZod3Schema(schema)) {
127+
return convertZod3Schema(schema, options);
188128
}
189129

190-
try {
191-
// Try Effect first, then @effect/schema
192-
let module;
193-
try {
194-
module = await import("effect");
195-
} catch {}
130+
return undefined;
131+
}
196132

197-
if (module?.JSONSchema) {
198-
// @ts-ignore
199-
globalThis.__effectJsonSchema = { JSONSchema: module.JSONSchema };
200-
}
201-
} catch {
202-
// Effect conversion not available
203-
}
133+
function convertZod3Schema(schema: any, options?: ConversionOptions): JSONSchema | undefined {
134+
const useReferences = options?.useReferences ?? false;
135+
136+
return zodToJsonSchema(schema, {
137+
$refStrategy: useReferences ? "root" : "none",
138+
}) as JSONSchema;
204139
}
205140

206-
/**
207-
* Check if a schema can be converted to JSON Schema
208-
*/
209-
export function canConvertSchema(schema: Schema): boolean {
210-
const result = schemaToJsonSchema(schema);
211-
return result !== undefined;
141+
function convertZod4Schema(schema: any, options?: ConversionOptions): JSONSchema | undefined {
142+
const useReferences = options?.useReferences ?? false;
143+
144+
return z4.toJSONSchema(schema, {
145+
target: "draft-7",
146+
io: "output",
147+
reused: useReferences ? "ref" : "inline",
148+
}) as JSONSchema;
212149
}
213150

214-
/**
215-
* Get the detected schema type
216-
*/
217-
export function detectSchemaType(schema: Schema): ConversionResult["schemaType"] {
218-
const result = schemaToJsonSchema(schema);
219-
return result?.schemaType ?? "unknown";
151+
function isYupSchema(schema: any): boolean {
152+
return "spec" in schema && "_typeCheck" in schema;
220153
}
221154

222-
/**
223-
* Check if the conversion libraries are initialized
224-
*/
225-
export function areConvertersInitialized(): {
226-
zod: boolean;
227-
yup: boolean;
228-
effect: boolean;
229-
} {
230-
return {
231-
// @ts-ignore
232-
zod: typeof globalThis.__zodToJsonSchema !== "undefined",
233-
// @ts-ignore
234-
yup: typeof globalThis.__yupToJsonSchema !== "undefined",
235-
// @ts-ignore
236-
effect: typeof globalThis.__effectJsonSchema !== "undefined",
237-
};
155+
function convertYupSchema(schema: any): JSONSchema | undefined {
156+
try {
157+
return convertSchema(schema) as JSONSchema;
158+
} catch {
159+
return undefined;
160+
}
161+
}
162+
163+
function isEffectSchema(schema: any): boolean {
164+
return "ast" in schema && typeof schema.ast === "object" && typeof schema.ast._tag === "string";
165+
}
166+
167+
function convertEffectSchema(schema: any): JSONSchema | undefined {
168+
try {
169+
return EffectJSONSchema.make(schema) as JSONSchema;
170+
} catch {
171+
return undefined;
172+
}
238173
}

0 commit comments

Comments
 (0)