Skip to content
Closed
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/fix-zod4-date-json-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/schema-to-json": patch
---

Support serializing Zod 4 `z.date()` schemas to JSON Schema format.
12 changes: 12 additions & 0 deletions packages/schema-to-json/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
},
Comment on lines +159 to +165

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Older Zod versions weaken schemas

With Zod 4.0–4.4, the unrepresentable function disables throwing for non-date types. Those releases only compare the option with "throw", so unsupported schemas become {} and accept rejected values.

Learn more

The package accepts Zod ^4.0.0, but function-valued unrepresentable handlers were added only in later Zod 4 releases. Earlier releases treat every value except the literal "throw" as permissive behavior. The callback therefore never runs there, although the existing override still converts dates. Other unrepresentable nodes lose their previous errors and emit unconstrained schemas.

Example: With Zod 4.1, converting z.symbol() previously throws. After this change it emits {}, which permits strings and numbers even though z.symbol() rejects them.

Recommended fix: Either raise both Zod dependency ranges to the first version supporting UnrepresentableHandler, or preserve compatibility by detecting that capability and explicitly throwing from override for every non-date unrepresentable node on older releases. Add a test installed against the minimum supported Zod 4 version.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

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) => {
Expand Down
41 changes: 41 additions & 0 deletions packages/schema-to-json/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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);
Expand Down