From ccad17af7d7908441f99cffd0b16afa1abe08ed6 Mon Sep 17 00:00:00 2001 From: sufiyan733 Date: Sun, 20 Sep 2026 23:14:26 +0530 Subject: [PATCH] fix(schema-to-json): support serializing Zod 4 z.date() to JSON Schema (#4939) In Zod 4, toJSONSchema delegates Date schemas to handleUnrepresentable, which threw 'Date cannot be represented in JSON Schema' when unrepresentable was omitted. Configure unrepresentable to return { type: 'string', format: 'date-time' } for date schemas to match Zod 3 output while preserving strict error handling for other unrepresentable types. --- .changeset/fix-zod4-date-json-schema.md | 5 +++ packages/schema-to-json/src/index.ts | 12 ++++++ packages/schema-to-json/tests/index.test.ts | 41 +++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 .changeset/fix-zod4-date-json-schema.md diff --git a/.changeset/fix-zod4-date-json-schema.md b/.changeset/fix-zod4-date-json-schema.md new file mode 100644 index 00000000000..11a45f0008b --- /dev/null +++ b/.changeset/fix-zod4-date-json-schema.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/schema-to-json": patch +--- + +Support serializing Zod 4 `z.date()` schemas to JSON Schema format. diff --git a/packages/schema-to-json/src/index.ts b/packages/schema-to-json/src/index.ts index 1c611388932..c5feb9a6c6a 100644 --- a/packages/schema-to-json/src/index.ts +++ b/packages/schema-to-json/src/index.ts @@ -156,12 +156,24 @@ function convertZod4Schema(schema: any, options?: ConversionOptions): JSONSchema target: "draft-7", io: "output", reused: useReferences ? "ref" : "inline", + unrepresentable: ({ zodSchema }) => { + const def = (zodSchema as any)._zod?.def; + if (def?.type === "date") { + return { type: "string", format: "date-time" }; + } + return "throw"; + }, override: ({ zodSchema, jsonSchema }) => { const def = zodSchema._zod.def; if (def.type === "undefined") { throw new Error("Undefined cannot be represented in JSON Schema"); } + if (def.type === "date") { + jsonSchema.type = "string"; + jsonSchema.format = "date-time"; + } + if (def.type === "object" && jsonSchema.required) { // Early Zod 4 permalinks do not propagate optional output through unions. const required = jsonSchema.required.filter((key) => { diff --git a/packages/schema-to-json/tests/index.test.ts b/packages/schema-to-json/tests/index.test.ts index b54788bf1a8..17161d4c28c 100644 --- a/packages/schema-to-json/tests/index.test.ts +++ b/packages/schema-to-json/tests/index.test.ts @@ -110,6 +110,45 @@ describe("schemaToJsonSchema", () => { "Undefined cannot be represented in JSON Schema" ); }); + + it("converts date schemas to string with date-time format", () => { + const schema = z.date(); + const result = schemaToJsonSchema(schema); + + expect(result).toBeDefined(); + expect(result?.jsonSchema).toMatchObject({ + type: "string", + format: "date-time", + }); + }); + + it("converts object with date schemas and wrappers", () => { + const schema = z.object({ + createdAt: z.date(), + updatedAt: z.date().optional(), + deletedAt: z.date().nullable(), + history: z.array(z.date()), + }); + + const result = schemaToJsonSchema(schema); + + expect(result).toBeDefined(); + expect(result?.jsonSchema).toMatchObject({ + type: "object", + properties: { + createdAt: { type: "string", format: "date-time" }, + updatedAt: { type: "string", format: "date-time" }, + deletedAt: { + anyOf: [{ type: "string", format: "date-time" }, { type: "null" }], + }, + history: { + type: "array", + items: { type: "string", format: "date-time" }, + }, + }, + required: ["createdAt", "deletedAt", "history"], + }); + }); }); it("preserves explicitly required metadata on current optional schemas", () => { @@ -290,6 +329,8 @@ describe("schemaToJsonSchema", () => { describe("canConvertSchema", () => { it("should return true for supported schemas", () => { expect(canConvertSchema(z3.string())).toBe(true); + expect(canConvertSchema(z3.date())).toBe(true); + expect(canConvertSchema(z4.date())).toBe(true); expect(canConvertSchema(y.string())).toBe(true); expect(canConvertSchema(type("string"))).toBe(true); expect(canConvertSchema(Schema.String)).toBe(true);