diff --git a/.changeset/zod-4-support.md b/.changeset/zod-4-support.md new file mode 100644 index 00000000000..ef90f6396e3 --- /dev/null +++ b/.changeset/zod-4-support.md @@ -0,0 +1,14 @@ +--- +"@trigger.dev/build": minor +"@trigger.dev/core": minor +"@trigger.dev/react-hooks": minor +"@trigger.dev/rsc": minor +"@trigger.dev/sdk": minor +"trigger.dev": minor +"@trigger.dev/redis-worker": minor +"@trigger.dev/schema-to-json": minor +--- + +Add Zod 4 support. You can now use Zod 3.25.76+ or Zod 4. + +Zod remains a runtime dependency of packages that execute schemas, so existing and new installations continue to receive it automatically. The matching peer dependency range allows package managers to reuse either a compatible Zod 3 or Zod 4 installation from your project. diff --git a/AGENTS.md b/AGENTS.md index cc947c7a57d..e459a74c9c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -108,7 +108,7 @@ When modifying only server components (`apps/webapp/`, `apps/supervisor/`, etc.) ## Dependency Pinning -Zod is pinned to a single version across the entire monorepo (currently `3.25.76`). When adding zod to a new or existing package, use the **exact same version** as the rest of the repo - never a different version or a range. Mismatched zod versions cause runtime type incompatibilities (e.g., schemas from one package can't be used as body validators in another). +Zod is pinned to a single version across the entire monorepo (currently `4.4.3`). When adding zod to a new or existing package, use the **exact same version** as the rest of the repo - never a different version or a range. Mismatched zod versions cause runtime type incompatibilities (e.g., schemas from one package can't be used as body validators in another). ## Architecture Overview diff --git a/apps/supervisor/package.json b/apps/supervisor/package.json index 2725fe2b729..d4555061708 100644 --- a/apps/supervisor/package.json +++ b/apps/supervisor/package.json @@ -23,7 +23,7 @@ "prom-client": "^15.1.0", "socket.io": "4.7.4", "std-env": "^3.8.0", - "zod": "3.25.76" + "zod": "4.4.3" }, "devDependencies": { "@internal/testcontainers": "workspace:*", diff --git a/apps/webapp/app/components/Feedback.tsx b/apps/webapp/app/components/Feedback.tsx index b679b042da1..d7204dde41a 100644 --- a/apps/webapp/app/components/Feedback.tsx +++ b/apps/webapp/app/components/Feedback.tsx @@ -5,7 +5,7 @@ import { getTextareaProps, useForm, } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { InformationCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"; import { EnvelopeIcon, ShieldCheckIcon } from "@heroicons/react/24/solid"; import { Form, useActionData, useLocation, useNavigation, useSearchParams } from "@remix-run/react"; @@ -129,7 +129,7 @@ export function Feedback({
{type === "feature" && ( diff --git a/apps/webapp/app/components/billing/BillingAlertsSection.tsx b/apps/webapp/app/components/billing/BillingAlertsSection.tsx index ce2f5e5e0fc..7d85e9880b0 100644 --- a/apps/webapp/app/components/billing/BillingAlertsSection.tsx +++ b/apps/webapp/app/components/billing/BillingAlertsSection.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm, type SubmissionResult } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { PlusIcon, TrashIcon } from "@heroicons/react/20/solid"; import { Form, useActionData, useSearchParams } from "@remix-run/react"; import { Fragment, useEffect, useMemo, useRef, useState } from "react"; diff --git a/apps/webapp/app/components/billing/BillingLimitConfigSection.tsx b/apps/webapp/app/components/billing/BillingLimitConfigSection.tsx index e6362d4cb1d..675f444c6f6 100644 --- a/apps/webapp/app/components/billing/BillingLimitConfigSection.tsx +++ b/apps/webapp/app/components/billing/BillingLimitConfigSection.tsx @@ -1,6 +1,6 @@ import { getFormProps, useForm, type SubmissionResult } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { Form, useActionData } from "@remix-run/react"; import { useEffect, useMemo, useRef, useState } from "react"; import { z } from "zod"; @@ -37,7 +37,7 @@ export const billingLimitFormSchema = z.discriminatedUnion("mode", [ z.object({ mode: z.literal("custom"), amount: z.coerce - .number({ invalid_type_error: "Not a valid amount" }) + .number({ error: "Not a valid amount" }) .positive("Amount must be greater than 0"), cancelInProgressRuns: z .preprocess((v) => v === "on" || v === true || v === "true", z.boolean()) diff --git a/apps/webapp/app/components/billing/BillingLimitRecoveryPanel.tsx b/apps/webapp/app/components/billing/BillingLimitRecoveryPanel.tsx index 1dfcdd73707..c0ecbc402c2 100644 --- a/apps/webapp/app/components/billing/BillingLimitRecoveryPanel.tsx +++ b/apps/webapp/app/components/billing/BillingLimitRecoveryPanel.tsx @@ -1,5 +1,5 @@ import { getFormProps, useForm, type SubmissionResult } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { Form, useActionData, useNavigation } from "@remix-run/react"; import { useEffect, useMemo, useRef, useState } from "react"; import { z } from "zod"; @@ -21,7 +21,7 @@ export const billingLimitRecoveryFormSchema = z .object({ action: z.enum(["increase", "remove"]), newAmount: z.coerce - .number({ invalid_type_error: "Not a valid amount" }) + .number({ error: "Not a valid amount" }) .positive("Amount must be greater than 0") .optional(), resumeMode: z.enum(["queue", "new_only"]), @@ -87,7 +87,7 @@ export function BillingLimitRecoveryPanel({ }, defaultValue: { action: "increase", - newAmount: suggestedNewLimitDollars, + newAmount: String(suggestedNewLimitDollars), resumeMode: "queue", }, }); diff --git a/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx b/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx index b126e77b34b..fe43b7a794f 100644 --- a/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx +++ b/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx @@ -1,6 +1,6 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; import { GlobeLinesIcon } from "~/assets/icons/GlobeLinesIcon"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { EnvelopeIcon, HashtagIcon, LockClosedIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { BellAlertIcon } from "@heroicons/react/24/solid"; import { useFetcher, useNavigate } from "@remix-run/react"; diff --git a/apps/webapp/app/components/metrics/QueryWidget.tsx b/apps/webapp/app/components/metrics/QueryWidget.tsx index 9f821816046..564f3e27718 100644 --- a/apps/webapp/app/components/metrics/QueryWidget.tsx +++ b/apps/webapp/app/components/metrics/QueryWidget.tsx @@ -55,7 +55,7 @@ const chartConfigOptions = { sortByColumn: z.string().nullable(), sortDirection: SortDirection, aggregation: AggregationType, - seriesColors: z.record(z.string()).optional(), + seriesColors: z.record(z.string(), z.string()).optional(), }; const ChartConfiguration = z.object({ ...chartConfigOptions }); diff --git a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx index 86747f9e277..a54344bad03 100644 --- a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx +++ b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { RectangleStackIcon } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; import { Form, useActionData, useNavigation, useParams, useSubmit } from "@remix-run/react"; diff --git a/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx b/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx index 3054a0cdf48..428389e129b 100644 --- a/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx +++ b/apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx @@ -1,5 +1,5 @@ import { getFormProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { EnvelopeIcon } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; import { useFetcher } from "@remix-run/react"; diff --git a/apps/webapp/app/models/orgIntegration.server.ts b/apps/webapp/app/models/orgIntegration.server.ts index f2bd0feebce..f136c9cb483 100644 --- a/apps/webapp/app/models/orgIntegration.server.ts +++ b/apps/webapp/app/models/orgIntegration.server.ts @@ -22,7 +22,7 @@ const SlackSecretSchema = z.object({ refreshToken: z.string().optional(), botScopes: z.array(z.string()).optional(), userScopes: z.array(z.string()).optional(), - raw: z.record(z.any()).optional(), + raw: z.record(z.string(), z.any()).optional(), }); type SlackSecret = z.infer; diff --git a/apps/webapp/app/models/vercelIntegration.server.ts b/apps/webapp/app/models/vercelIntegration.server.ts index 3a1aaf4b8ea..324a1651270 100644 --- a/apps/webapp/app/models/vercelIntegration.server.ts +++ b/apps/webapp/app/models/vercelIntegration.server.ts @@ -129,7 +129,7 @@ export const VercelSecretSchema = z.object({ teamId: z.string().nullable().optional(), userId: z.string().optional(), installationId: z.string().optional(), - raw: z.record(z.any()).optional(), + raw: z.record(z.string(), z.any()).optional(), }); export type VercelSecret = z.infer; diff --git a/apps/webapp/app/models/vercelSdkRecovery.server.ts b/apps/webapp/app/models/vercelSdkRecovery.server.ts index d3e1bfd6961..31253fe430e 100644 --- a/apps/webapp/app/models/vercelSdkRecovery.server.ts +++ b/apps/webapp/app/models/vercelSdkRecovery.server.ts @@ -145,11 +145,11 @@ export const VercelSchemas = { .union([ z .object({ - envs: z.array(z.record(z.unknown())), + envs: z.array(z.record(z.string(), z.unknown())), pagination: z.unknown().optional(), }) .passthrough(), - z.array(z.record(z.unknown())), + z.array(z.record(z.string(), z.unknown())), ]) .transform((val) => (Array.isArray(val) ? { envs: val } : val)), diff --git a/apps/webapp/app/presenters/v3/MetricDashboardPresenter.server.ts b/apps/webapp/app/presenters/v3/MetricDashboardPresenter.server.ts index 0b84e971b2f..5c43a7ff63c 100644 --- a/apps/webapp/app/presenters/v3/MetricDashboardPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/MetricDashboardPresenter.server.ts @@ -48,7 +48,7 @@ export const DashboardLayout = z.discriminatedUnion("version", [ z.object({ version: z.literal("1"), layout: z.array(LayoutItem), - widgets: z.record(Widget), + widgets: z.record(z.string(), Widget), }), ]); diff --git a/apps/webapp/app/presenters/v3/SpanPresenter.server.ts b/apps/webapp/app/presenters/v3/SpanPresenter.server.ts index b680a1c6a8e..209e906e50f 100644 --- a/apps/webapp/app/presenters/v3/SpanPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/SpanPresenter.server.ts @@ -1024,6 +1024,9 @@ export class SpanPresenter extends BasePresenter { filePath: lockedWorker?.lockedBy?.filePath ?? "", }, run: { + // zod v4 types the run-context `context` (z.any) field as required; it was + // never populated here (optional under v3), so undefined preserves behavior. + context: undefined, id: run.friendlyId, createdAt: run.createdAt, tags: run.runTags, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx index b6fe0b09c68..b51a6d5ae1a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { ArrowUpCircleIcon, EnvelopeIcon, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx index f121900e901..275f6556eb3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx @@ -1,11 +1,11 @@ import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { HashtagIcon, LockClosedIcon } from "@heroicons/react/20/solid"; import { Form, useActionData, useNavigate, useNavigation } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/router"; import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { SlackIcon } from "@trigger.dev/companyicons"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { InlineCode } from "~/components/code/InlineCode"; @@ -233,7 +233,6 @@ export default function Page() { const project = useProject(); const environment = useEnvironment(); const [currentAlertChannel, setCurrentAlertChannel] = useState(option ?? "EMAIL"); - const formRef = useRef(null); const [selectedSlackChannelValue, setSelectedSlackChannelValue] = useState(); @@ -267,7 +266,7 @@ export default function Page() { if (navigation.state !== "idle") return; if (lastSubmission !== undefined) return; - formRef.current?.reset(); + form.reset(); }, [navigation.state, lastSubmission]); return ( @@ -281,7 +280,7 @@ export default function Page() { > New alert -
+
- {formEnvironments.errors} + {formEnvironments.errors} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx index 02d6ef00c4d..1c397e3eddf 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, useForm, type FieldMetadata, type FormMetadata } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { LockClosedIcon, LockOpenIcon, @@ -88,7 +88,7 @@ const schema = z.object({ return; }, - z.array(z.string(), { required_error: "At least one environment is required" }) + z.array(z.string(), { error: "At least one environment is required" }) ), variables: z.preprocess((i) => { if (!Array.isArray(i)) { @@ -247,9 +247,9 @@ export default function Page() { const [selectedEnvironmentIds, setSelectedEnvironmentIds] = useState>(new Set()); const [selectedBranchId, setSelectedBranchId] = useState(undefined); - // TODO for no we only support branch-specific env vars for Preview environments - // Mostly to keep the UX for setting consistent env-vars across Dev/Staging/Prod easier - const previewBranches = environments.filter( + // TODO: we only support branch-specific env vars for Preview environments. + // This keeps setting consistent env vars across Dev/Staging/Prod easier. + const branchEnvironments = environments.filter( (env) => env.type === "PREVIEW" && env.parentEnvironmentId !== null ); const nonBranchEnvironments = environments.filter((env) => env.parentEnvironmentId === null); @@ -436,7 +436,7 @@ export default function Page() { value={selectedBranchId ?? "all"} setValue={handleBranchChange} placeholder="All branches" - items={[{ id: "all", branchName: "All branches" }, ...previewBranches]} + items={[{ id: "all", branchName: "All branches" }, ...branchEnvironments]} className="w-fit min-w-52" filter={{ keys: [ @@ -444,7 +444,7 @@ export default function Page() { ], }} text={(val) => - val ? previewBranches.find((b) => b.id === val)?.branchName : null + val ? branchEnvironments.find((b) => b.id === val)?.branchName : null } dropdownIcon > diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx index bd152e58033..d53c28df3cb 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { BookOpenIcon, InformationCircleIcon, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx index 6ec1aa7fce1..1b23b80390b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { BellAlertIcon } from "@heroicons/react/20/solid"; import { type MetaFunction, useFetcher, useRevalidator } from "@remix-run/react"; import { type ActionFunctionArgs, json, type LoaderFunctionArgs } from "@remix-run/server-runtime"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx index 769a4868266..7ed9502b375 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { Outlet, useNavigate } from "@remix-run/react"; import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { useCallback } from "react"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx index 4f5a2c1e52d..5c8a7b77fd7 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx @@ -160,7 +160,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { if (!parsed.success) { return typedjson( { - error: parsed.error.errors.map((e) => e.message).join(", "), + error: parsed.error.issues.map((e) => e.message).join(", "), rows: null, columns: null, stats: null, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx index a3a0ef05470..3d154e26289 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { useLocation } from "@remix-run/react"; import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.general/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.general/route.tsx index ccc61fc5ac9..c2f3ec2aa22 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.general/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.general/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { conformZodMessage, parseWithZod } from "@conform-to/zod"; +import { conformZodMessage, parseWithZod } from "@conform-to/zod/v4"; import { ExclamationTriangleIcon, FolderIcon, TrashIcon } from "@heroicons/react/20/solid"; import { Form, useActionData, useNavigation } from "@remix-run/react"; import { json } from "@remix-run/server-runtime"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx index 9bc54b19bf1..706aad4d860 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { Form, useActionData, useNavigation, useSearchParams } from "@remix-run/react"; import { json } from "@remix-run/server-runtime"; import React, { useCallback, useEffect, useRef, useState } from "react"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index ffe6dbcd038..87ddb127205 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { CheckCircleIcon, RectangleStackIcon, @@ -433,37 +433,15 @@ function StandardTaskForm({ const [showTemplateCreatedSuccessMessage, setShowTemplateCreatedSuccessMessage] = useState(false); - const [ - form, - { - environmentId, - payload, - metadata, - taskIdentifier, - delaySeconds, - ttlSeconds, - idempotencyKey, - idempotencyKeyTTLSeconds, - queue, - concurrencyKey, - maxAttempts, - maxDurationSeconds, - triggerSource, - tags, - version, - machine, - region, - prioritySeconds, - }, - ] = useForm({ + const [form, fields] = useForm({ id: "test-task", // TODO: type this lastResult: lastSubmission as any, onSubmit(event, { formData }) { event.preventDefault(); - formData.set(payload.name, currentPayloadJson.current); - formData.set(metadata.name, currentMetadataJson.current); + formData.set(fields.payload.name, currentPayloadJson.current); + formData.set(fields.metadata.name, currentMetadataJson.current); submit(formData, { method: "POST" }); }, @@ -471,6 +449,24 @@ function StandardTaskForm({ return parseWithZod(formData, { schema: TestTaskData }); }, }); + const { + environmentId, + taskIdentifier, + delaySeconds, + ttlSeconds, + idempotencyKey, + idempotencyKeyTTLSeconds, + queue, + concurrencyKey, + maxAttempts, + maxDurationSeconds, + triggerSource, + tags, + version, + machine, + region, + prioritySeconds, + } = fields; return ( diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx index 348658ba9ef..29c1e0c26f8 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx @@ -1,6 +1,6 @@ import colorWheelIcon from "../../assets/images/color-wheel.png"; import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { conformZodMessage, parseWithZod } from "@conform-to/zod"; +import { conformZodMessage, parseWithZod } from "@conform-to/zod/v4"; import { CheckIcon, ExclamationTriangleIcon, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsx index a7790c4ee48..c61bbe72b8f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsx @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import type { MetaFunction } from "@remix-run/react"; import { json, redirect } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx index 1dc9753ac13..888d30c681a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { Form, useActionData, useParams, type MetaFunction } from "@remix-run/react"; import { json, type ActionFunction, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core/utils"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx index cc2a95571d0..2212a69828d 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { EnvelopeIcon, NoSymbolIcon, UserPlusIcon } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; import { @@ -1062,7 +1062,7 @@ export function PurchaseSeatsModal({ Total extra seats {canCreateV3Projects ? ( ) : ( )} diff --git a/apps/webapp/app/routes/_app.orgs.new/route.tsx b/apps/webapp/app/routes/_app.orgs.new/route.tsx index b06d9bafd93..d676d21a338 100644 --- a/apps/webapp/app/routes/_app.orgs.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.new/route.tsx @@ -1,6 +1,6 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; import { GlobeLinesIcon } from "~/assets/icons/GlobeLinesIcon"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { BuildingOffice2Icon } from "@heroicons/react/20/solid"; import { RadioGroup } from "@radix-ui/react-radio-group"; import { json, redirect, type ActionFunction, type LoaderFunctionArgs } from "@remix-run/node"; diff --git a/apps/webapp/app/routes/account._index/route.tsx b/apps/webapp/app/routes/account._index/route.tsx index 4ecccb33d68..94969a30239 100644 --- a/apps/webapp/app/routes/account._index/route.tsx +++ b/apps/webapp/app/routes/account._index/route.tsx @@ -1,6 +1,6 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; import { useEffect, useState } from "react"; -import { conformZodMessage, parseWithZod } from "@conform-to/zod"; +import { conformZodMessage, parseWithZod } from "@conform-to/zod/v4"; import { ComputerDesktopIcon, MoonIcon, SunIcon, SwatchIcon } from "@heroicons/react/20/solid"; import { Form, @@ -90,9 +90,9 @@ function createSchema( ) { return z.object({ name: z - .string({ required_error: "You must enter a name" }) + .string({ error: "You must enter a name" }) .min(2, "Your name must be at least 2 characters long") - .max(50), + .max(50, "Your name must be 50 characters or less"), email: emailSchema.pipe( z.string().superRefine((email, ctx) => { if (constraints.isEmailUnique === undefined) { diff --git a/apps/webapp/app/routes/account.tokens/route.tsx b/apps/webapp/app/routes/account.tokens/route.tsx index 1179d070ead..63baccf8f2b 100644 --- a/apps/webapp/app/routes/account.tokens/route.tsx +++ b/apps/webapp/app/routes/account.tokens/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { BookOpenIcon, ShieldCheckIcon, TrashIcon } from "@heroicons/react/20/solid"; import { ShieldExclamationIcon } from "@heroicons/react/24/solid"; import { DialogClose } from "@radix-ui/react-dialog"; @@ -168,9 +168,9 @@ const CreateTokenSchema = z.discriminatedUnion("action", [ z.object({ action: z.literal("create"), tokenName: z - .string({ required_error: "You must enter a name" }) + .string({ error: "You must enter a name" }) .min(2, "Your name must be at least 2 characters long") - .max(50), + .max(50, "Your name must be 50 characters or less"), // Optional — when no RBAC plugin is installed the UI hides the // dropdown and submits no roleId; the action passes that through // and createPersonalAccessToken just doesn't write a TokenRole. diff --git a/apps/webapp/app/routes/admin.feature-flags.tsx b/apps/webapp/app/routes/admin.feature-flags.tsx index 76ba62ff8e9..f26c85520a9 100644 --- a/apps/webapp/app/routes/admin.feature-flags.tsx +++ b/apps/webapp/app/routes/admin.feature-flags.tsx @@ -87,7 +87,7 @@ export const action = dashboardAction( return json({ error: "Invalid JSON body" }, { status: 400 }); } - const payloadSchema = z.object({ flags: z.record(z.unknown()) }); + const payloadSchema = z.object({ flags: z.record(z.string(), z.unknown()) }); const parsed = payloadSchema.safeParse(body); if (!parsed.success) { return json({ error: "Invalid payload" }, { status: 400 }); diff --git a/apps/webapp/app/routes/api.v1.plain.customer-cards.ts b/apps/webapp/app/routes/api.v1.plain.customer-cards.ts index 5e09fb08544..ea7c3abf123 100644 --- a/apps/webapp/app/routes/api.v1.plain.customer-cards.ts +++ b/apps/webapp/app/routes/api.v1.plain.customer-cards.ts @@ -106,7 +106,7 @@ export async function action({ request }: ActionFunctionArgs) { if (!parsed.success) { logger.warn("Invalid Plain customer card request", { - errors: parsed.error.errors, + errors: parsed.error.issues, }); return json({ error: "Invalid request body" }, { status: 400 }); } diff --git a/apps/webapp/app/routes/api.v1.prompts.$slug.ts b/apps/webapp/app/routes/api.v1.prompts.$slug.ts index 316701581e5..68a3465c181 100644 --- a/apps/webapp/app/routes/api.v1.prompts.$slug.ts +++ b/apps/webapp/app/routes/api.v1.prompts.$slug.ts @@ -95,7 +95,7 @@ export const loader = createLoaderApiRoute( // POST /api/v1/prompts/:slug — Resolve prompt with variables const ResolveBody = z.object({ - variables: z.record(z.unknown()).default({}), + variables: z.record(z.string(), z.unknown()).default({}), label: z.string().optional(), version: z.number().optional(), }); diff --git a/apps/webapp/app/routes/api.v1.query.ts b/apps/webapp/app/routes/api.v1.query.ts index 2f4011455ba..f08b9dd1583 100644 --- a/apps/webapp/app/routes/api.v1.query.ts +++ b/apps/webapp/app/routes/api.v1.query.ts @@ -93,7 +93,7 @@ const { action, loader } = createActionApiRoute( const { result, periodClipped: _periodClipped, maxQueryPeriod: _maxQueryPeriod } = queryResult; if (format === "csv") { - const csv = rowsToCSV(result.rows, result.columns); + const csv = rowsToCSV(result.rows as Record[], result.columns); return json({ format: "csv", diff --git a/apps/webapp/app/routes/api.v1.schedules.ts b/apps/webapp/app/routes/api.v1.schedules.ts index b5fd2fd968c..c7eb6f87281 100644 --- a/apps/webapp/app/routes/api.v1.schedules.ts +++ b/apps/webapp/app/routes/api.v1.schedules.ts @@ -108,6 +108,7 @@ export async function loader({ request }: LoaderFunctionArgs) { environmentId: authenticationResult.environment.id, page: params.data.page ?? 1, pageSize: params.data.perPage, + tasks: undefined, }); return { diff --git a/apps/webapp/app/routes/confirm-basic-details.tsx b/apps/webapp/app/routes/confirm-basic-details.tsx index 4e370913240..efea8ac4ad0 100644 --- a/apps/webapp/app/routes/confirm-basic-details.tsx +++ b/apps/webapp/app/routes/confirm-basic-details.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { conformZodMessage, parseWithZod } from "@conform-to/zod"; +import { conformZodMessage, parseWithZod } from "@conform-to/zod/v4"; import { ArrowRightIcon, EnvelopeIcon, UserIcon } from "@heroicons/react/20/solid"; import { HandRaisedIcon } from "@heroicons/react/24/solid"; import { RadioGroup } from "@radix-ui/react-radio-group"; @@ -316,7 +316,10 @@ export default function Page() { ) : ( <> - + )} diff --git a/apps/webapp/app/routes/invite-resend.tsx b/apps/webapp/app/routes/invite-resend.tsx index 3cba88bd20a..d64cc2d2aaf 100644 --- a/apps/webapp/app/routes/invite-resend.tsx +++ b/apps/webapp/app/routes/invite-resend.tsx @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { json } from "@remix-run/server-runtime"; import { env } from "process"; import { z } from "zod"; diff --git a/apps/webapp/app/routes/invite-revoke.tsx b/apps/webapp/app/routes/invite-revoke.tsx index fc1e4d0b79e..d4ab69f9c83 100644 --- a/apps/webapp/app/routes/invite-revoke.tsx +++ b/apps/webapp/app/routes/invite-revoke.tsx @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { json } from "@remix-run/server-runtime"; import { z } from "zod"; import { $replica } from "~/db.server"; diff --git a/apps/webapp/app/routes/invites.tsx b/apps/webapp/app/routes/invites.tsx index 6cf667c8677..acc0a09f4a3 100644 --- a/apps/webapp/app/routes/invites.tsx +++ b/apps/webapp/app/routes/invites.tsx @@ -1,5 +1,5 @@ import { getFormProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { EnvelopeIcon } from "@heroicons/react/20/solid"; import { type ActionFunction, type LoaderFunctionArgs, json, redirect } from "@remix-run/node"; import { Form, useActionData } from "@remix-run/react"; diff --git a/apps/webapp/app/routes/login._index/route.tsx b/apps/webapp/app/routes/login._index/route.tsx index ce186c73461..a3d402078e8 100644 --- a/apps/webapp/app/routes/login._index/route.tsx +++ b/apps/webapp/app/routes/login._index/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { EnvelopeIcon, LockClosedIcon } from "@heroicons/react/20/solid"; import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Form, useNavigation } from "@remix-run/react"; @@ -38,9 +38,7 @@ import { cn } from "~/utils/cn"; // format validation in the browser and renders the styled inline error. // Server-side errors still surface via the session-backed authError below. const emailSchema = z.object({ - email: z - .string({ required_error: "Enter your email address" }) - .email("Enter a valid email address"), + email: z.string({ error: "Enter your email address" }).email("Enter a valid email address"), }); function LastUsedBadge({ className }: { className?: string }) { diff --git a/apps/webapp/app/routes/login.sso/route.tsx b/apps/webapp/app/routes/login.sso/route.tsx index 1ed85497353..f3c3b0a4188 100644 --- a/apps/webapp/app/routes/login.sso/route.tsx +++ b/apps/webapp/app/routes/login.sso/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { ArrowLeftIcon, ArrowUpRightIcon } from "@heroicons/react/20/solid"; import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Form, useNavigation } from "@remix-run/react"; @@ -67,7 +67,7 @@ const ERROR_MESSAGES: Record = { // inline error before submit. Server-side errors keep flowing via ?error=. const ssoEmailSchema = z.object({ email: z - .string({ required_error: "Enter your enterprise email address" }) + .string({ error: "Enter your enterprise email address" }) .email("Enter a valid email address"), }); diff --git a/apps/webapp/app/routes/resources.$projectId.deployments.$deploymentShortCode.cancel.ts b/apps/webapp/app/routes/resources.$projectId.deployments.$deploymentShortCode.cancel.ts index 239d0982f9e..6acf14777c0 100644 --- a/apps/webapp/app/routes/resources.$projectId.deployments.$deploymentShortCode.cancel.ts +++ b/apps/webapp/app/routes/resources.$projectId.deployments.$deploymentShortCode.cancel.ts @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { json } from "@remix-run/node"; import { errAsync, fromPromise, okAsync } from "neverthrow"; import { z } from "zod"; diff --git a/apps/webapp/app/routes/resources.$projectId.deployments.$deploymentShortCode.promote.ts b/apps/webapp/app/routes/resources.$projectId.deployments.$deploymentShortCode.promote.ts index 979264516d1..529f627e5fb 100644 --- a/apps/webapp/app/routes/resources.$projectId.deployments.$deploymentShortCode.promote.ts +++ b/apps/webapp/app/routes/resources.$projectId.deployments.$deploymentShortCode.promote.ts @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { json } from "@remix-run/node"; import { z } from "zod"; import { $replica, prisma } from "~/db.server"; diff --git a/apps/webapp/app/routes/resources.$projectId.deployments.$deploymentShortCode.rollback.ts b/apps/webapp/app/routes/resources.$projectId.deployments.$deploymentShortCode.rollback.ts index d109e97c9d5..3caed9013b3 100644 --- a/apps/webapp/app/routes/resources.$projectId.deployments.$deploymentShortCode.rollback.ts +++ b/apps/webapp/app/routes/resources.$projectId.deployments.$deploymentShortCode.rollback.ts @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { json } from "@remix-run/node"; import { z } from "zod"; import { $replica, prisma } from "~/db.server"; diff --git a/apps/webapp/app/routes/resources.batches.$batchId.check-completion.ts b/apps/webapp/app/routes/resources.batches.$batchId.check-completion.ts index 2d1c6bc702b..3c7cd64523b 100644 --- a/apps/webapp/app/routes/resources.batches.$batchId.check-completion.ts +++ b/apps/webapp/app/routes/resources.batches.$batchId.check-completion.ts @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import type { ActionFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { z } from "zod"; diff --git a/apps/webapp/app/routes/resources.branches.archive.tsx b/apps/webapp/app/routes/resources.branches.archive.tsx index bf9e05a0d64..dfc542f793f 100644 --- a/apps/webapp/app/routes/resources.branches.archive.tsx +++ b/apps/webapp/app/routes/resources.branches.archive.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { DialogClose } from "@radix-ui/react-dialog"; import { Form, useActionData, useLocation } from "@remix-run/react"; import { type ActionFunctionArgs } from "@remix-run/server-runtime"; @@ -100,10 +100,13 @@ export function ArchiveButton({ {...getFormProps(form)} className="w-full" > - + This will permanently make this branch{" "} @@ -116,7 +119,7 @@ export function ArchiveButton({ Once archived you can create a new branch with the same name. - {form.errors} + {form.errors?.join(", ")} diff --git a/apps/webapp/app/routes/resources.branches.create.tsx b/apps/webapp/app/routes/resources.branches.create.tsx index 9fa5aaeace8..23a8b822d19 100644 --- a/apps/webapp/app/routes/resources.branches.create.tsx +++ b/apps/webapp/app/routes/resources.branches.create.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { DialogClose } from "@radix-ui/react-dialog"; import { useFetcher, useLocation, useSearchParams } from "@remix-run/react"; import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; diff --git a/apps/webapp/app/routes/resources.feedback.ts b/apps/webapp/app/routes/resources.feedback.ts index 4ace262f0cb..41f0f24249d 100644 --- a/apps/webapp/app/routes/resources.feedback.ts +++ b/apps/webapp/app/routes/resources.feedback.ts @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { uiComponent } from "@team-plain/ui-components"; import { z } from "zod"; @@ -56,8 +56,7 @@ const feedbackTypeLiterals = Object.keys(feedbackTypes).map((key) => z.literal(k const feedbackType = z.union( [feedbackTypeLiterals[0], feedbackTypeLiterals[1], ...feedbackTypeLiterals.slice(2)], { - required_error: "Must be either 'bug' or 'feature'", - invalid_type_error: "Must be either 'bug' or 'feature'", + error: "Must be a valid feedback type", } ); diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github.tsx index 4840a424d8c..e5dfd81a97e 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { ArrowUpCircleIcon, ArrowUpRightIcon, diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction.tsx index 5bef068b994..66c5d92297a 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction.tsx @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { ArrowPathIcon, InformationCircleIcon } from "@heroicons/react/20/solid"; import { XCircleIcon } from "@heroicons/react/24/outline"; import { Form } from "@remix-run/react"; diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx index 61ce814dbd5..32de08aca6d 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx @@ -1,5 +1,5 @@ import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { type FetcherWithComponents, @@ -196,7 +196,7 @@ export function UpsertScheduleForm({ if (!result.success) { cronPatternResult = { isValid: false, - error: result.error.errors[0].message, + error: result.error.issues[0].message, }; } else { try { diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx index 792275a917d..019a28db063 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx @@ -1,5 +1,5 @@ import { getFormProps, useForm } from "@conform-to/react"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { CheckCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; import { Form, useActionData, useFetcher, useLocation, useNavigation } from "@remix-run/react"; diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route.tsx index e1ed1b2cc70..cf3940fdfe5 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route.tsx @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { Form, useLocation, useNavigation, useSubmit } from "@remix-run/react"; import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import type { WaitpointTokenStatus } from "@trigger.dev/core/v3"; diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.schedules-addon.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.schedules-addon.ts index d61bea07045..88f27b4d47c 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.schedules-addon.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.schedules-addon.ts @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core/v3"; import { z } from "zod"; diff --git a/apps/webapp/app/routes/resources.sessions.$sessionParam.close.ts b/apps/webapp/app/routes/resources.sessions.$sessionParam.close.ts index 26f573399c9..b0f820fccc7 100644 --- a/apps/webapp/app/routes/resources.sessions.$sessionParam.close.ts +++ b/apps/webapp/app/routes/resources.sessions.$sessionParam.close.ts @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { type ActionFunction, json } from "@remix-run/node"; import { z } from "zod"; import { $replica, prisma } from "~/db.server"; diff --git a/apps/webapp/app/routes/resources.taskruns.$runParam.cancel.ts b/apps/webapp/app/routes/resources.taskruns.$runParam.cancel.ts index cf6b0317780..7610f2c099c 100644 --- a/apps/webapp/app/routes/resources.taskruns.$runParam.cancel.ts +++ b/apps/webapp/app/routes/resources.taskruns.$runParam.cancel.ts @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { json } from "@remix-run/node"; import { z } from "zod"; import { prisma } from "~/db.server"; diff --git a/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts b/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts index e91ac2c5451..73e45b94d5d 100644 --- a/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts +++ b/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts @@ -1,4 +1,4 @@ -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { json, type LoaderFunctionArgs } from "@remix-run/node"; import { type EnvironmentType, prettyPrintPacket } from "@trigger.dev/core/v3"; import { type Prisma } from "@trigger.dev/database"; diff --git a/apps/webapp/app/services/apiAuth.server.ts b/apps/webapp/app/services/apiAuth.server.ts index 90fc9d2d3b7..8f2d4abad8e 100644 --- a/apps/webapp/app/services/apiAuth.server.ts +++ b/apps/webapp/app/services/apiAuth.server.ts @@ -768,7 +768,7 @@ export async function validateJWTTokenAndRenew( return; } - const renewedJwt = await renewJWTToken(payload.data); + const renewedJwt = await renewJWTToken(payload.data as Record); return { payload: payload.data, @@ -807,7 +807,7 @@ export async function validateJWTTokenAndRenew( } const renewedJwt = await generateJWTTokenForEnvironment(authenticatedEnv.environment, { - ...payload.data, + ...(payload.data as Record), }); // The environment carries secret material; log only non-secret fields. diff --git a/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts b/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts index 5fcd6eb5450..349c8cf0d7e 100644 --- a/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts +++ b/apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts @@ -118,7 +118,7 @@ async function resolveLimitConfig( if (!parsedOverride.success) { logger.error("Error parsing rate limiter override", { override, - errors: parsedOverride.error.errors, + errors: parsedOverride.error.issues, }); return defaultLimiter; diff --git a/apps/webapp/app/services/billingLimit.schemas.ts b/apps/webapp/app/services/billingLimit.schemas.ts index 6628e5cb22c..8ffdca3fd57 100644 --- a/apps/webapp/app/services/billingLimit.schemas.ts +++ b/apps/webapp/app/services/billingLimit.schemas.ts @@ -1,4 +1,3 @@ -import type { BillingClient } from "@trigger.dev/platform"; import { z } from "zod"; /** @@ -175,7 +174,7 @@ export type BillingLimitPageData = BillingLimitResult & { currentSpendCents: number; }; -/** Bridge webapp Zod schemas to BillingClient.fetch (separate Zod type instances). */ -export function asPlatformSchema(schema: z.ZodTypeAny) { - return schema as unknown as Parameters[1]; +/** Preserve a concrete schema type when passing it to BillingClient.fetch. */ +export function asPlatformSchema(schema: TSchema): TSchema { + return schema; } diff --git a/apps/webapp/app/services/platform.v3.server.ts b/apps/webapp/app/services/platform.v3.server.ts index f8b524e3cdd..a533cafaf42 100644 --- a/apps/webapp/app/services/platform.v3.server.ts +++ b/apps/webapp/app/services/platform.v3.server.ts @@ -242,7 +242,7 @@ const MachineOverrideValues = z.object({ }); type MachineOverrideValues = z.infer; -const MachineOverrides = z.record(MachinePresetName, MachineOverrideValues.partial()); +const MachineOverrides = z.partialRecord(MachinePresetName, MachineOverrideValues.partial()); type MachineOverrides = z.infer; const MachinePresetOverrides = z.object({ diff --git a/apps/webapp/app/services/queryService.server.ts b/apps/webapp/app/services/queryService.server.ts index 755da8ec3a3..77f2b3f4178 100644 --- a/apps/webapp/app/services/queryService.server.ts +++ b/apps/webapp/app/services/queryService.server.ts @@ -381,7 +381,7 @@ export async function executeQuery( const result = await executeTSQL(queryClickhouse.reader, { ...baseOptions, - schema: z.record(z.any()), + schema: z.record(z.string(), z.any()) as unknown as TOut, tableSchema: effectiveSchemas, transformValues: true, enforcedWhereClause, diff --git a/apps/webapp/app/services/routeBuilders/apiBuilder.server.ts b/apps/webapp/app/services/routeBuilders/apiBuilder.server.ts index 83212a6e8b2..fe01c3504e6 100644 --- a/apps/webapp/app/services/routeBuilders/apiBuilder.server.ts +++ b/apps/webapp/app/services/routeBuilders/apiBuilder.server.ts @@ -127,7 +127,7 @@ async function rejectRestrictedKeyWithoutAuthorization({ ); } -type AnyZodSchema = z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion; +type AnyZodSchema = z.ZodType; // A multi-resource auth check has two possible directions, and route authors // have to pick one explicitly: diff --git a/apps/webapp/app/services/routeBuilders/dashboardBuilder.server.ts b/apps/webapp/app/services/routeBuilders/dashboardBuilder.server.ts index 01bdb6d9b53..42d8acd4bf2 100644 --- a/apps/webapp/app/services/routeBuilders/dashboardBuilder.server.ts +++ b/apps/webapp/app/services/routeBuilders/dashboardBuilder.server.ts @@ -12,7 +12,7 @@ import type { AuthorizationOption, DashboardLoaderOptions, SessionUser } from ". import { fromZodError } from "zod-validation-error"; import type { z } from "zod"; -type AnyZodSchema = z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion; +type AnyZodSchema = z.ZodType; function loginRedirectFor(request: Request, override?: string): Response { if (override) return redirect(override); diff --git a/apps/webapp/app/services/routeBuilders/dashboardBuilder.ts b/apps/webapp/app/services/routeBuilders/dashboardBuilder.ts index a4603895915..b2b8d72f10b 100644 --- a/apps/webapp/app/services/routeBuilders/dashboardBuilder.ts +++ b/apps/webapp/app/services/routeBuilders/dashboardBuilder.ts @@ -17,7 +17,7 @@ import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-r import type { RbacAbility, RbacResource } from "@trigger.dev/rbac"; import type { z } from "zod"; -type AnyZodSchema = z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion; +type AnyZodSchema = z.ZodType; type InferZod = T extends z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion ? z.infer diff --git a/apps/webapp/app/utils/json.ts b/apps/webapp/app/utils/json.ts index b3b44055958..e32960a08af 100644 --- a/apps/webapp/app/utils/json.ts +++ b/apps/webapp/app/utils/json.ts @@ -15,7 +15,7 @@ export function safeJsonParse(json?: string): unknown { export function safeJsonZodParse( schema: z.Schema, json: string -): z.SafeParseReturnType | undefined { +): z.ZodSafeParseResult | undefined { const parsed = safeJsonParse(json); if (parsed === null) { @@ -51,7 +51,7 @@ export async function safeBodyFromResponse( export async function safeParseBodyFromResponse( response: Response, schema: z.Schema -): Promise | undefined> { +): Promise | undefined> { try { const unknownJson = await response.json(); diff --git a/apps/webapp/app/utils/timeGranularity.ts b/apps/webapp/app/utils/timeGranularity.ts index 0c8ecc41ac8..4e4d542de3a 100644 --- a/apps/webapp/app/utils/timeGranularity.ts +++ b/apps/webapp/app/utils/timeGranularity.ts @@ -6,7 +6,7 @@ const DurationString = z.string().refine( const ms = parseDuration(val); return ms !== null && ms > 0; }, - (val) => ({ message: `Invalid or non-positive duration string: "${val}"` }) + { error: (issue) => `Invalid or non-positive duration string: "${issue.input}"` } ); const BracketSchema = z.object({ diff --git a/apps/webapp/app/v3/featureFlags.server.ts b/apps/webapp/app/v3/featureFlags.server.ts index c79bd8c01d7..1a435a48e52 100644 --- a/apps/webapp/app/v3/featureFlags.server.ts +++ b/apps/webapp/app/v3/featureFlags.server.ts @@ -37,7 +37,7 @@ export function makeFlag(_prisma: PrismaClientOrTransaction = prisma) { const parsed = flagSchema.safeParse(opts.overrides[opts.key]); if (parsed.success) { - return parsed.data; + return parsed.data as z.infer<(typeof FeatureFlagCatalog)[T]>; } } @@ -45,7 +45,7 @@ export function makeFlag(_prisma: PrismaClientOrTransaction = prisma) { const parsed = flagSchema.safeParse(value.value); if (parsed.success) { - return parsed.data; + return parsed.data as z.infer<(typeof FeatureFlagCatalog)[T]>; } } diff --git a/apps/webapp/app/v3/featureFlags.ts b/apps/webapp/app/v3/featureFlags.ts index f617cab16f3..d6ba1fb191f 100644 --- a/apps/webapp/app/v3/featureFlags.ts +++ b/apps/webapp/app/v3/featureFlags.ts @@ -112,8 +112,10 @@ export type FeatureFlagCatalog = z.infer; export function validateFeatureFlagValue( key: T, value: unknown -): z.SafeParseReturnType> { - return FeatureFlagCatalog[key].safeParse(value); +): ReturnType<(typeof FeatureFlagCatalog)[T]["safeParse"]> { + return FeatureFlagCatalog[key].safeParse(value) as ReturnType< + (typeof FeatureFlagCatalog)[T]["safeParse"] + >; } // Utility function to validate all feature flags at once @@ -162,23 +164,28 @@ export type FlagControlType = | { type: "number"; min?: number; max?: number } | { type: "string" }; -export function getFlagControlType(schema: z.ZodTypeAny): FlagControlType { - const typeName = schema._def.typeName; +export function getFlagControlType(schema: z.ZodType): FlagControlType { + // zod v4: schema.def.type is a lowercase tag ("boolean"/"enum"/"number"/...). + const def = schema.def as { + type: string; + checks?: Array<{ _zod?: { def?: { check?: string; value?: number } } }>; + }; - if (typeName === "ZodBoolean") { + if (def.type === "boolean") { return { type: "boolean" }; } - if (typeName === "ZodEnum") { - return { type: "enum", options: schema._def.values as string[] }; + if (def.type === "enum") { + return { type: "enum", options: (schema as z.ZodEnum).options as string[] }; } - // z.coerce.number() reports as ZodNumber; pull min/max out of its checks - // so the UI can render a constrained number input instead of free text. - if (typeName === "ZodNumber") { - const checks = (schema._def.checks ?? []) as Array<{ kind: string; value?: number }>; - const min = checks.find((c) => c.kind === "min")?.value; - const max = checks.find((c) => c.kind === "max")?.value; + // z.coerce.number() reports as "number"; pull min/max out of its v4 checks + // (each check carries `_zod.def.{check,value}`) so the UI can render a + // constrained number input instead of free text. + if (def.type === "number") { + const checks = def.checks ?? []; + const min = checks.find((c) => c._zod?.def?.check === "greater_than")?._zod?.def?.value; + const max = checks.find((c) => c._zod?.def?.check === "less_than")?._zod?.def?.value; return { type: "number", min, max }; } diff --git a/apps/webapp/app/v3/schedules.ts b/apps/webapp/app/v3/schedules.ts index 1653e053544..e0d33c77654 100644 --- a/apps/webapp/app/v3/schedules.ts +++ b/apps/webapp/app/v3/schedules.ts @@ -20,28 +20,25 @@ export const CronPattern = z.string().refine( return false; } }, - (val) => { - const parts = val.split(" "); - if (parts.length > 5) { - return { - message: "CRON expressions with seconds are not allowed", - }; - } + { + error: (issue) => { + const val = String(issue.input); + const parts = val.split(" "); + if (parts.length > 5) { + return "CRON expressions with seconds are not allowed"; + } - if (val === "") { - return { - message: "CRON expression is required", - }; - } + if (val === "") { + return "CRON expression is required"; + } - try { - parseExpression(val); - return { - message: "Unknown problem", - }; - } catch (e) { - return { message: e instanceof Error ? e.message : JSON.stringify(e) }; - } + try { + parseExpression(val); + return "Unknown problem"; + } catch (e) { + return e instanceof Error ? e.message : JSON.stringify(e); + } + }, } ); diff --git a/apps/webapp/app/v3/services/aiRunFilterService.server.ts b/apps/webapp/app/v3/services/aiRunFilterService.server.ts index 96b74ab3314..c3bc2b10e8b 100644 --- a/apps/webapp/app/v3/services/aiRunFilterService.server.ts +++ b/apps/webapp/app/v3/services/aiRunFilterService.server.ts @@ -258,7 +258,7 @@ export class AIRunFilterService { const validationResult = AIFilters.safeParse(output.filters); if (!validationResult.success) { logger.error("AI filter validation failed", { - errors: validationResult.error.errors, + errors: validationResult.error.issues, filters: output.filters, }); diff --git a/apps/webapp/app/v3/services/replayTaskRun.server.ts b/apps/webapp/app/v3/services/replayTaskRun.server.ts index d626ec2ae8c..8e71026252c 100644 --- a/apps/webapp/app/v3/services/replayTaskRun.server.ts +++ b/apps/webapp/app/v3/services/replayTaskRun.server.ts @@ -20,7 +20,9 @@ type OverrideOptions = { metadata?: unknown; bulkActionId?: string; triggerSource?: string; -} & RunOptionsData; + // zod v4 types `.optional().transform()` fields as required-with-undefined in the + // output; Partial keeps them omittable for callers passing a subset of overrides. +} & Partial; export class ReplayTaskRunService extends BaseService { public async call(existingTaskRun: TaskRun, overrideOptions: OverrideOptions = {}) { diff --git a/apps/webapp/app/v3/utils/zodPubSub.server.ts b/apps/webapp/app/v3/utils/zodPubSub.server.ts index c473443d169..6b1132a8715 100644 --- a/apps/webapp/app/v3/utils/zodPubSub.server.ts +++ b/apps/webapp/app/v3/utils/zodPubSub.server.ts @@ -54,7 +54,7 @@ class RedisZodSubscriber< eventName: K, listener: (payload: z.infer) => Promise ): void { - this._listeners.set(eventName as string, listener); + this._listeners.set(eventName as string, listener as Parameters[1]); } public async stopListening(): Promise { diff --git a/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts b/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts index 1399b87fc2b..cb55583c518 100644 --- a/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts +++ b/apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts @@ -71,7 +71,7 @@ export type TriggerEnvironmentType = z.infer; * Only explicitly `false` entries disable sync. */ export const SyncEnvVarsMappingSchema = z - .record(EnvSlugSchema, z.record(z.string(), z.boolean())) + .partialRecord(EnvSlugSchema, z.record(z.string(), z.boolean())) .default({}); export type SyncEnvVarsMapping = z.infer; diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 90dc46ac088..0a43858a722 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -120,7 +120,7 @@ "@trigger.dev/core": "workspace:*", "@trigger.dev/database": "workspace:*", "@trigger.dev/otlp-importer": "workspace:*", - "@trigger.dev/platform": "1.3.0", + "@trigger.dev/platform": "1.3.1", "@trigger.dev/plugins": "workspace:*", "@trigger.dev/rbac": "workspace:*", "@trigger.dev/redis-worker": "workspace:*", @@ -216,9 +216,9 @@ "util": "^0.12.5", "uuid": "^14.0.0", "ws": "^8.11.0", - "zod": "3.25.76", - "zod-error": "1.5.0", - "zod-validation-error": "^1.5.0" + "zod": "4.4.3", + "zod-error": "2.0.0", + "zod-validation-error": "^5.0.0" }, "devDependencies": { "@internal/clickhouse": "workspace:*", diff --git a/apps/webapp/test/billingLimitsRoute.test.ts b/apps/webapp/test/billingLimitsRoute.test.ts index 66f1ac5a7b5..278ee90d791 100644 --- a/apps/webapp/test/billingLimitsRoute.test.ts +++ b/apps/webapp/test/billingLimitsRoute.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { parseWithZod } from "@conform-to/zod"; +import { parseWithZod } from "@conform-to/zod/v4"; import { billingAlertsSchema } from "~/components/billing/BillingAlertsSection"; import { billingLimitFormSchema, diff --git a/internal-packages/clickhouse/package.json b/internal-packages/clickhouse/package.json index ec5ec25e667..719b979ba90 100644 --- a/internal-packages/clickhouse/package.json +++ b/internal-packages/clickhouse/package.json @@ -10,8 +10,8 @@ "@internal/tracing": "workspace:*", "@internal/tsql": "workspace:*", "@trigger.dev/core": "workspace:*", - "zod": "3.25.76", - "zod-error": "1.5.0" + "zod": "4.4.3", + "zod-error": "2.0.0" }, "devDependencies": { "@internal/testcontainers": "workspace:*", diff --git a/internal-packages/clickhouse/src/client/tsql.ts b/internal-packages/clickhouse/src/client/tsql.ts index f61009237e4..99e6e402bc3 100644 --- a/internal-packages/clickhouse/src/client/tsql.ts +++ b/internal-packages/clickhouse/src/client/tsql.ts @@ -221,7 +221,7 @@ export async function executeTSQL( const queryFn = reader.queryWithStats({ name: isExplain ? `${options.name}-explain` : options.name, query: queryToExecute, - params: z.record(z.any()), + params: z.record(z.string(), z.any()), // EXPLAIN returns rows with an 'explain' column schema: isExplain ? z.object({ explain: z.string() }) : options.schema, settings: options.clickhouseSettings, @@ -257,7 +257,7 @@ export async function executeTSQL( const additionalQueryFn = reader.queryWithStats({ name: `${options.name}-explain-${explainType.name.toLowerCase()}`, query: explainType.query, - params: z.record(z.any()), + params: z.record(z.string(), z.any()), schema: z.object({ explain: z.string() }), settings: options.clickhouseSettings, logFields: { tsql: options.query }, diff --git a/internal-packages/clickhouse/src/tsqlFunctions.test.ts b/internal-packages/clickhouse/src/tsqlFunctions.test.ts index 8c1f9ec4221..188fd58e875 100644 --- a/internal-packages/clickhouse/src/tsqlFunctions.test.ts +++ b/internal-packages/clickhouse/src/tsqlFunctions.test.ts @@ -101,7 +101,7 @@ async function assertQueryExecutes(client: ClickhouseClient, tsqlQuery: string): const [error] = await executeTSQL(client, { name: "func-test", query: tsqlQuery, - schema: z.record(z.any()), + schema: z.record(z.string(), z.any()), enforcedWhereClause, tableSchema: [taskRunsSchema], }); diff --git a/internal-packages/compute/package.json b/internal-packages/compute/package.json index 4e221879382..c5045878f54 100644 --- a/internal-packages/compute/package.json +++ b/internal-packages/compute/package.json @@ -14,7 +14,7 @@ } }, "dependencies": { - "zod": "3.25.76" + "zod": "4.4.3" }, "devDependencies": { "rimraf": "6.0.1" diff --git a/internal-packages/compute/src/types.ts b/internal-packages/compute/src/types.ts index 5d8d5e26af9..fd878334efa 100644 --- a/internal-packages/compute/src/types.ts +++ b/internal-packages/compute/src/types.ts @@ -15,7 +15,7 @@ export const TemplateCreateRequestSchema = z.object({ callback: z .object({ url: z.string(), - metadata: z.record(z.string()).optional(), + metadata: z.record(z.string(), z.string()).optional(), }) .optional(), }); @@ -38,14 +38,14 @@ export type TemplateCreateResponse = z.infer; @@ -58,7 +58,7 @@ export type InstanceCreateResponse = z.infer; @@ -67,13 +67,13 @@ export type InstanceSnapshotRequest = z.infer; @@ -82,14 +82,14 @@ export const SnapshotCallbackPayloadSchema = z.discriminatedUnion("status", [ status: z.literal("completed"), snapshot_id: z.string(), instance_id: z.string(), - metadata: z.record(z.string()).optional(), + metadata: z.record(z.string(), z.string()).optional(), duration_ms: z.number().optional(), }), z.object({ status: z.literal("failed"), instance_id: z.string(), error: z.string().optional(), - metadata: z.record(z.string()).optional(), + metadata: z.record(z.string(), z.string()).optional(), duration_ms: z.number().optional(), }), ]); diff --git a/internal-packages/dashboard-agent/package.json b/internal-packages/dashboard-agent/package.json index c0776e25f11..3bfe5f3f4a6 100644 --- a/internal-packages/dashboard-agent/package.json +++ b/internal-packages/dashboard-agent/package.json @@ -14,7 +14,7 @@ "@internal/dashboard-agent-db": "workspace:*", "@trigger.dev/sdk": "workspace:*", "ai": "^6.0.116", - "zod": "3.25.76" + "zod": "4.4.3" }, "devDependencies": { "@ai-sdk/provider": "3.0.8", diff --git a/internal-packages/emails/package.json b/internal-packages/emails/package.json index 33dfe4c1d71..9f61faab457 100644 --- a/internal-packages/emails/package.json +++ b/internal-packages/emails/package.json @@ -18,7 +18,7 @@ "react-dom": "^18.2.0", "resend": "^3.2.0", "tiny-invariant": "^1.2.0", - "zod": "3.25.76" + "zod": "4.4.3" }, "devDependencies": { "@types/nodemailer": "^8.0.0", diff --git a/internal-packages/run-engine/package.json b/internal-packages/run-engine/package.json index 516e6a18696..026463ce0aa 100644 --- a/internal-packages/run-engine/package.json +++ b/internal-packages/run-engine/package.json @@ -32,7 +32,7 @@ "nanoid": "3.3.8", "redlock": "5.0.0-beta.2", "seedrandom": "^3.0.5", - "zod": "3.25.76", + "zod": "4.4.3", "p-map": "^6.0.0" }, "devDependencies": { diff --git a/internal-packages/run-engine/src/batch-queue/types.ts b/internal-packages/run-engine/src/batch-queue/types.ts index 9aa085e286a..0a8494abd00 100644 --- a/internal-packages/run-engine/src/batch-queue/types.ts +++ b/internal-packages/run-engine/src/batch-queue/types.ts @@ -37,7 +37,7 @@ export const BatchItem = z.object({ */ payloadType: z.string().optional(), /** Options for this specific item - stored as JSON */ - options: z.record(z.unknown()).optional(), + options: z.record(z.string(), z.unknown()).optional(), }); export type BatchItem = z.infer; @@ -68,7 +68,7 @@ export const BatchMeta = z.object({ /** Trigger version */ triggerVersion: z.string().optional(), /** Trace context */ - traceContext: z.record(z.unknown()).optional(), + traceContext: z.record(z.string(), z.unknown()).optional(), /** Whether span parent should be a link */ spanParentAsLink: z.boolean().optional(), /** Realtime streams version */ @@ -103,7 +103,7 @@ export const BatchItemFailure = z.object({ */ payload: z.string().optional(), /** The options that were used */ - options: z.record(z.unknown()).optional(), + options: z.record(z.string(), z.unknown()).optional(), /** Error message */ error: z.string(), /** Error code if available */ diff --git a/internal-packages/schedule-engine/package.json b/internal-packages/schedule-engine/package.json index 52a27a11e27..b8a53a75f6c 100644 --- a/internal-packages/schedule-engine/package.json +++ b/internal-packages/schedule-engine/package.json @@ -22,7 +22,7 @@ "cron-parser": "^4.9.0", "cronstrue": "^2.50.0", "nanoid": "3.3.8", - "zod": "3.25.76" + "zod": "4.4.3" }, "devDependencies": { "@internal/testcontainers": "workspace:*", diff --git a/internal-packages/tsql/package.json b/internal-packages/tsql/package.json index 0cac36e7b27..3c86630738f 100644 --- a/internal-packages/tsql/package.json +++ b/internal-packages/tsql/package.json @@ -8,7 +8,7 @@ "dependencies": { "@trigger.dev/core": "workspace:*", "antlr4ts": "0.5.0-alpha.4", - "zod": "3.25.76" + "zod": "4.4.3" }, "scripts": { "typecheck": "tsc --noEmit", diff --git a/package.json b/package.json index 2708b06be73..0718cb0178f 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,8 @@ "brace-expansion@>=2 <2.0.3": "2.0.3", "brace-expansion@>=5 <5.0.6": "5.0.6", "@jsonhero/json-infer-types>ip-address": "^10.2.0", - "@modelcontextprotocol/sdk@>=1.26.0>express-rate-limit": "^8.6.0" + "@modelcontextprotocol/sdk@>=1.26.0>express-rate-limit": "^8.6.0", + "zod": "4.4.3" }, "onlyBuiltDependencies": [ "@depot/cli", diff --git a/packages/cli-v3/package.json b/packages/cli-v3/package.json index 6d0d90da98f..1051d4666c8 100644 --- a/packages/cli-v3/package.json +++ b/packages/cli-v3/package.json @@ -146,8 +146,8 @@ "tinyglobby": "^0.2.10", "ws": "^8.18.0", "xdg-app-paths": "^8.3.0", - "zod": "3.25.76", - "zod-validation-error": "^1.5.0" + "zod": "4.4.3", + "zod-validation-error": "^5.0.0" }, "engines": { "node": ">=18.20.0" diff --git a/packages/cli-v3/src/cli/common.ts b/packages/cli-v3/src/cli/common.ts index 18d61a05803..dbb706e06c6 100644 --- a/packages/cli-v3/src/cli/common.ts +++ b/packages/cli-v3/src/cli/common.ts @@ -42,7 +42,7 @@ export async function handleTelemetry(action: () => Promise) { } } -export async function wrapCommandAction( +export async function wrapCommandAction, TResult>( name: string, schema: T, options: unknown, @@ -55,7 +55,7 @@ export async function wrapCommandAction( throw new Error(fromZodError(parsedOptions.error).toString()); } - logger.loggerLevel = parsedOptions.data.logLevel; + logger.loggerLevel = parsedOptions.data.logLevel as typeof logger.loggerLevel; logger.debug(`Running "${name}" with the following options`, { options: options, diff --git a/packages/cli-v3/src/commands/analyze.ts b/packages/cli-v3/src/commands/analyze.ts index dc03d36e2e5..e52112a53bb 100644 --- a/packages/cli-v3/src/commands/analyze.ts +++ b/packages/cli-v3/src/commands/analyze.ts @@ -113,14 +113,14 @@ const ImportSchema = z.object({ kind: ImportKind, external: z.boolean().optional(), original: z.string().optional(), - with: z.record(z.string()).optional(), + with: z.record(z.string(), z.string()).optional(), }); const InputSchema = z.object({ bytes: z.number(), imports: z.array(ImportSchema), format: z.enum(["cjs", "esm"]).optional(), - with: z.record(z.string()).optional(), + with: z.record(z.string(), z.string()).optional(), }); const OutputImportSchema = z.object({ diff --git a/packages/cli-v3/src/mcp/tools/agentChat.ts b/packages/cli-v3/src/mcp/tools/agentChat.ts index 992e3d436a1..3f5b390c046 100644 --- a/packages/cli-v3/src/mcp/tools/agentChat.ts +++ b/packages/cli-v3/src/mcp/tools/agentChat.ts @@ -70,7 +70,7 @@ const StartAgentChatInput = CommonProjectsInput.extend({ .describe("A unique conversation ID. Reuse to resume a conversation.") .optional(), clientData: z - .record(z.unknown()) + .record(z.string(), z.unknown()) .describe("Client data to include with every message (e.g. userId, model).") .optional(), preload: z diff --git a/packages/cli-v3/src/utilities/configFiles.ts b/packages/cli-v3/src/utilities/configFiles.ts index 4fee41d9cc4..541a58a68ad 100644 --- a/packages/cli-v3/src/utilities/configFiles.ts +++ b/packages/cli-v3/src/utilities/configFiles.ts @@ -22,13 +22,13 @@ const CliConfigProfileSettings = z.object({ }); type CliConfigProfileSettings = z.infer; -const OldCliConfigFile = z.record(CliConfigProfileSettings); +const OldCliConfigFile = z.record(z.string(), CliConfigProfileSettings); type OldCliConfigFile = z.infer; const CliConfigFile = z.object({ version: z.literal(2), currentProfile: z.string().default(DEFFAULT_PROFILE), - profiles: z.record(CliConfigProfileSettings), + profiles: z.record(z.string(), CliConfigProfileSettings), settings: z .object({ hasSeenMCPInstallPrompt: z.boolean().optional(), diff --git a/packages/core/package.json b/packages/core/package.json index 292d4ff421d..7f941cf573e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -225,9 +225,8 @@ "std-env": "^3.8.1", "tinyexec": "^0.3.2", "uncrypto": "^0.1.3", - "zod": "3.25.76", - "zod-error": "1.5.0", - "zod-validation-error": "^1.5.0" + "zod": "^3.25.76 || ^4.0.0", + "zod-validation-error": "^5.0.0" }, "devDependencies": { "@ai-sdk/provider-utils": "^1.0.22", @@ -663,5 +662,8 @@ "type": "module", "main": "./dist/commonjs/index.js", "types": "./dist/commonjs/index.d.ts", - "module": "./dist/esm/index.js" + "module": "./dist/esm/index.js", + "peerDependencies": { + "zod": "^3.25.76 || ^4.0.0" + } } diff --git a/packages/core/src/schemas/eventFilter.ts b/packages/core/src/schemas/eventFilter.ts index 66b4d645466..77aa40867ff 100644 --- a/packages/core/src/schemas/eventFilter.ts +++ b/packages/core/src/schemas/eventFilter.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; export const stringPatternMatchers = [ z.object({ @@ -65,7 +65,7 @@ type EventMatcher = z.infer; export type EventFilter = { [key: string]: EventMatcher | EventFilter }; export const EventFilterSchema: z.ZodType = z.lazy(() => - z.record(z.union([EventMatcherSchema, EventFilterSchema])) + z.record(z.string(), z.union([EventMatcherSchema, EventFilterSchema])) ); export const EventRuleSchema = z.object({ diff --git a/packages/core/src/schemas/json.ts b/packages/core/src/schemas/json.ts index 79384db156a..672eb44cc5f 100644 --- a/packages/core/src/schemas/json.ts +++ b/packages/core/src/schemas/json.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; const LiteralSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); type Literal = z.infer; @@ -6,7 +6,11 @@ type Literal = z.infer; export type DeserializedJson = Literal | { [key: string]: DeserializedJson } | DeserializedJson[]; export const DeserializedJsonSchema: z.ZodType = z.lazy(() => - z.union([LiteralSchema, z.array(DeserializedJsonSchema), z.record(DeserializedJsonSchema)]) + z.union([ + LiteralSchema, + z.array(DeserializedJsonSchema), + z.record(z.string(), DeserializedJsonSchema), + ]) ); const SerializableSchema = z.union([ @@ -26,5 +30,9 @@ export type SerializableJson = | SerializableJson[]; export const SerializableJsonSchema: z.ZodType = z.lazy(() => - z.union([SerializableSchema, z.array(SerializableJsonSchema), z.record(SerializableJsonSchema)]) + z.union([ + SerializableSchema, + z.array(SerializableJsonSchema), + z.record(z.string(), SerializableJsonSchema), + ]) ); diff --git a/packages/core/src/v3/apiClient/core.ts b/packages/core/src/v3/apiClient/core.ts index c6425e7ee37..0bdd0f01f09 100644 --- a/packages/core/src/v3/apiClient/core.ts +++ b/packages/core/src/v3/apiClient/core.ts @@ -1,5 +1,6 @@ -import { z } from "zod"; -import { fromZodError, ValidationError } from "zod-validation-error"; +import { z as z3 } from "zod/v3"; +import { z } from "zod/v4"; +import { fromZodError, ValidationError } from "zod-validation-error/v4"; import type { RetryOptions } from "../schemas/index.js"; import { calculateNextRetryDelay } from "../utils/retries.js"; import { ApiConnectionError, ApiError, ApiSchemaValidationError } from "./errors.js"; @@ -12,6 +13,7 @@ import { SemanticInternalAttributes } from "../semanticInternalAttributes.js"; import type { TriggerTracer } from "../tracer.js"; import { randomUUID } from "../utils/crypto.js"; import { accessoryAttributes } from "../utils/styleAttributes.js"; +import type { AnyZodSchema, inferZodSchemaOutput } from "../types/schemas.js"; import type { CursorPageParams, CursorPageResponse, @@ -71,16 +73,16 @@ interface FetchOffsetLimitPageParams extends OffsetLimitPageParams { query?: URLSearchParams; } -export function zodfetch( +export function zodfetch( schema: TResponseBodySchema, url: string, requestInit?: RequestInit, - options?: ZodFetchOptions> -): ApiPromise> { + options?: ZodFetchOptions> +): ApiPromise> { return new ApiPromise(_doZodFetch(schema, url, requestInit, options)); } -export function zodfetchCursorPage( +export function zodfetchCursorPage( schema: TItemSchema, url: string, params: FetchCursorPageParams, @@ -101,13 +103,22 @@ export function zodfetchCursorPage( query.set("page[before]", params.before); } - const cursorPageSchema = z.object({ - data: z.array(schema), - pagination: z.object({ - next: z.string().optional(), - previous: z.string().optional(), - }), - }); + const cursorPageSchema = + "_zod" in schema + ? z.object({ + data: z.array(schema as any), + pagination: z.object({ + next: z.string().optional(), + previous: z.string().optional(), + }), + }) + : z3.object({ + data: z3.array(schema as any), + pagination: z3.object({ + next: z3.string().optional(), + previous: z3.string().optional(), + }), + }); const $url = new URL(url); $url.search = query.toString(); @@ -117,7 +128,7 @@ export function zodfetchCursorPage( return new CursorPagePromise(fetchResult, schema, url, params, requestInit, options); } -export function zodfetchOffsetLimitPage( +export function zodfetchOffsetLimitPage( schema: TItemSchema, url: string, params: FetchOffsetLimitPageParams, @@ -134,14 +145,24 @@ export function zodfetchOffsetLimitPage( query.set("page", String(params.page)); } - const offsetLimitPageSchema = z.object({ - data: z.array(schema), - pagination: z.object({ - currentPage: z.coerce.number(), - totalPages: z.coerce.number(), - count: z.coerce.number(), - }), - }); + const offsetLimitPageSchema = + "_zod" in schema + ? z.object({ + data: z.array(schema as any), + pagination: z.object({ + currentPage: z.coerce.number(), + totalPages: z.coerce.number(), + count: z.coerce.number(), + }), + }) + : z3.object({ + data: z3.array(schema as any), + pagination: z3.object({ + currentPage: z3.coerce.number(), + totalPages: z3.coerce.number(), + count: z3.coerce.number(), + }), + }); const $url = new URL(url); $url.search = query.toString(); @@ -149,7 +170,9 @@ export function zodfetchOffsetLimitPage( const fetchResult = _doZodFetch(offsetLimitPageSchema, $url.href, requestInit, options); return new OffsetLimitPagePromise( - fetchResult as Promise>>>, + fetchResult as Promise< + ZodFetchResult>> + >, schema, url, params, @@ -195,12 +218,12 @@ async function traceZodFetch( ); } -async function _doZodFetch( +async function _doZodFetch( schema: TResponseBodySchema, url: string, requestInit?: PromiseOrValue, - options?: ZodFetchOptions> -): Promise>> { + options?: ZodFetchOptions> +): Promise>> { let $requestInit = await requestInit; return traceZodFetch({ url, requestInit: $requestInit, options }, async (span) => { @@ -223,13 +246,13 @@ async function _doZodFetch( }); } -async function _doZodFetchWithRetries( +async function _doZodFetchWithRetries( schema: TResponseBodySchema, url: string, requestInit?: RequestInit, options?: ZodFetchOptions, attempt = 1 -): Promise>> { +): Promise>> { try { const response = await context.with(suppressTracing(context.active()), () => fetch(url, requestInitWithCache(requestInit)) @@ -470,12 +493,12 @@ export class ApiPromise extends Promise { } } -export class CursorPagePromise - extends ApiPromise>> - implements AsyncIterable> +export class CursorPagePromise + extends ApiPromise>> + implements AsyncIterable> { constructor( - result: Promise>>>, + result: Promise>>>, private schema: TItemSchema, private url: string, private params: FetchCursorPageParams, @@ -490,7 +513,9 @@ export class CursorPagePromise ); } - #fetchPage(params: Omit): Promise>> { + #fetchPage( + params: Omit + ): Promise>> { return zodfetchCursorPage( this.schema, this.url, @@ -515,12 +540,12 @@ export class CursorPagePromise } } -export class OffsetLimitPagePromise - extends ApiPromise>> - implements AsyncIterable> +export class OffsetLimitPagePromise + extends ApiPromise>> + implements AsyncIterable> { constructor( - result: Promise>>>, + result: Promise>>>, private schema: TItemSchema, private url: string, private params: FetchOffsetLimitPageParams, @@ -541,7 +566,7 @@ export class OffsetLimitPagePromise #fetchPage( params: Omit - ): Promise>> { + ): Promise>> { return zodfetchOffsetLimitPage( this.schema, this.url, @@ -644,17 +669,17 @@ function injectRequestIdempotencyKey( }; } -export type ZodFetchSSEMessageValueSchema< - TDiscriminatedUnion extends z.ZodDiscriminatedUnion, -> = z.ZodFirstPartySchemaTypes | TDiscriminatedUnion; +export type ZodFetchSSEMessageValueSchema = TSchema; export interface ZodFetchSSEMessageCatalogSchema { - [key: string]: ZodFetchSSEMessageValueSchema; + [key: string]: AnyZodSchema; } export type ZodFetchSSEMessageHandlers = Partial<{ - [K in keyof TCatalogSchema]: (payload: z.infer) => Promise | void; + [K in keyof TCatalogSchema]: ( + payload: inferZodSchemaOutput + ) => Promise | void; }>; export type ZodFetchSSEOptions = { @@ -699,6 +724,10 @@ export class ZodFetchSSEResult = statusCode?: number; }; -export async function wrapZodFetch( +export async function wrapZodFetch( schema: T, url: string, requestInit?: RequestInit, - options?: ZodFetchOptions> -): Promise>> { + options?: ZodFetchOptions> +): Promise>> { try { const response = await zodfetch(schema, url, requestInit, { retry: { diff --git a/packages/core/src/v3/apiClient/index.ts b/packages/core/src/v3/apiClient/index.ts index 23f13f37573..e2ce190e3f5 100644 --- a/packages/core/src/v3/apiClient/index.ts +++ b/packages/core/src/v3/apiClient/index.ts @@ -1,5 +1,5 @@ import { nanoid } from "nanoid"; -import { z } from "zod"; +import { z } from "zod/v4"; import { VERSION } from "../../version.js"; import { isAdditionalApiKey } from "../apiKeys.js"; import type { ApiClientConfiguration } from "../apiClientManager-api.js"; @@ -1903,7 +1903,7 @@ export class ApiClient { async generateJWTClaims(requestOptions?: ZodFetchOptions): Promise> { return zodfetch( - z.record(z.any()), + z.record(z.string(), z.any()), `${this.baseUrl}/api/v1/auth/jwt/claims`, { method: "POST", diff --git a/packages/core/src/v3/apiClient/stream.ts b/packages/core/src/v3/apiClient/stream.ts index ec35f1deb63..0124b140172 100644 --- a/packages/core/src/v3/apiClient/stream.ts +++ b/packages/core/src/v3/apiClient/stream.ts @@ -1,4 +1,4 @@ -import type { z } from "zod"; +import type { AnyZodSchema, inferZodSchemaOutput } from "../types/schemas.js"; import { type Offset, FetchError, @@ -22,12 +22,12 @@ export type ZodShapeStreamOptions = { onError?: (e: Error) => void; }; -export type ZodShapeStreamInstance = { - stream: AsyncIterableStream>; +export type ZodShapeStreamInstance = { + stream: AsyncIterableStream>; stop: (delay?: number) => void; }; -export function zodShapeStream( +export function zodShapeStream( schema: TShapeSchema, url: string, options?: ZodShapeStreamOptions @@ -59,7 +59,7 @@ export function zodShapeStream( const readableShape = new ReadableShapeStream(shapeStream); const stream = readableShape.stream.pipeThrough( - new TransformStream>({ + new TransformStream>({ async transform(chunk, controller) { const result = schema.safeParse(chunk); @@ -73,7 +73,7 @@ export function zodShapeStream( ); return { - stream: stream as AsyncIterableStream>, + stream: stream as AsyncIterableStream>, stop: (delay?: number) => { if (delay) { setTimeout(() => { diff --git a/packages/core/src/v3/apiClient/zodCompatibility.test.ts b/packages/core/src/v3/apiClient/zodCompatibility.test.ts new file mode 100644 index 00000000000..9d091a51395 --- /dev/null +++ b/packages/core/src/v3/apiClient/zodCompatibility.test.ts @@ -0,0 +1,65 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; +import { z as z3 } from "zod/v3"; +import { z as z4 } from "zod/v4"; +import { zodfetchCursorPage, zodfetchOffsetLimitPage } from "./core.js"; + +describe("Zod pagination compatibility", () => { + let server: Server; + let baseUrl: string; + + beforeAll(async () => { + server = createServer((request, response) => { + response.setHeader("content-type", "application/json"); + + if (request.url?.startsWith("/cursor")) { + response.end( + JSON.stringify({ + data: [{ value: "42" }], + pagination: {}, + }) + ); + return; + } + + response.end( + JSON.stringify({ + data: [{ value: "42" }], + pagination: { currentPage: "1", totalPages: "1", count: "1" }, + }) + ); + }); + + await new Promise((resolve) => { + server.listen(0, "127.0.0.1", () => { + const address = server.address() as AddressInfo; + baseUrl = `http://127.0.0.1:${address.port}`; + resolve(); + }); + }); + }); + + afterAll(async () => { + await new Promise((resolve) => server.close(() => resolve())); + }); + + it.each([ + ["Zod 3", z3.object({ value: z3.coerce.number() })], + ["Zod 4", z4.object({ value: z4.coerce.number() })], + ])("parses cursor pages with %s caller schemas", async (_name, schema) => { + const page = await zodfetchCursorPage(schema, `${baseUrl}/cursor`, {}); + + expect(page.data).toEqual([{ value: 42 }]); + }); + + it.each([ + ["Zod 3", z3.object({ value: z3.coerce.number() })], + ["Zod 4", z4.object({ value: z4.coerce.number() })], + ])("parses offset pages with %s caller schemas", async (_name, schema) => { + const page = await zodfetchOffsetLimitPage(schema, `${baseUrl}/offset`, {}); + + expect(page.data).toEqual([{ value: 42 }]); + expect(page.pagination).toEqual({ currentPage: 1, totalPages: 1, count: 1 }); + }); +}); diff --git a/packages/core/src/v3/errors.ts b/packages/core/src/v3/errors.ts index 1fc7c5bce6c..d42da7c5326 100644 --- a/packages/core/src/v3/errors.ts +++ b/packages/core/src/v3/errors.ts @@ -1,4 +1,4 @@ -import type { z } from "zod"; +import type { z } from "zod/v4"; import type { DeploymentErrorData } from "./schemas/api.js"; import type { WorkerManifest } from "./schemas/build.js"; import { ImportTaskFileErrors } from "./schemas/build.js"; @@ -8,6 +8,7 @@ import { TaskMetadataFailedToParseData } from "./schemas/messages.js"; import { links } from "./links.js"; import type { ExceptionEventProperties } from "./schemas/openTelemetry.js"; import { assertExhaustive } from "../utils.js"; +import type { ZodIssueLike } from "./types/schemas.js"; /** * If you throw this, it will get converted into an INTERNAL_ERROR @@ -484,7 +485,7 @@ function correctStackTraceLine(line: string, projectDir?: string, isDev?: boolea return line.trim(); } -export function groupTaskMetadataIssuesByTask(tasks: any, issues: z.ZodIssue[]) { +export function groupTaskMetadataIssuesByTask(tasks: any, issues: ReadonlyArray) { return issues.reduce( (acc, issue) => { if (issue.path.length === 0) { @@ -553,7 +554,7 @@ export class UncaughtExceptionError extends Error { export class TaskMetadataParseError extends Error { constructor( - public readonly zodIssues: z.ZodIssue[], + public readonly zodIssues: ReadonlyArray, public readonly tasks: any ) { super(`Failed to parse task metadata`); diff --git a/packages/core/src/v3/index.ts b/packages/core/src/v3/index.ts index 10551fca6f6..41af37e9754 100644 --- a/packages/core/src/v3/index.ts +++ b/packages/core/src/v3/index.ts @@ -83,8 +83,13 @@ export * from "./config.js"; export { getSchemaParseFn, type AnySchemaParseFn, + type AnyZodSchema, type SchemaParseFn, + type ZodErrorLike, + type ZodIssueLike, type inferSchemaOut, + type inferZodSchemaInput, + type inferZodSchemaOutput, isSchemaZodEsque, isSchemaValibotEsque, isSchemaArkTypeEsque, diff --git a/packages/core/src/v3/runEngineWorker/supervisor/http.ts b/packages/core/src/v3/runEngineWorker/supervisor/http.ts index 450be432577..a80edccc202 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/http.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/http.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import type { WorkerApiConnectRequestBody, WorkerApiDequeueRequestBody, @@ -27,6 +27,7 @@ import { wrapZodFetch, type ApiResult, type ZodFetchOptions } from "../../zodfet import { createHeaders } from "../util.js"; import { WORKER_HEADERS } from "../consts.js"; import { SimpleStructuredLogger } from "../../utils/structuredLogger.js"; +import type { AnyZodSchema, inferZodSchemaOutput } from "../../types/schemas.js"; type SupervisorHttpClientOptions = SupervisorClientCommonOptions; @@ -61,13 +62,13 @@ export class SupervisorHttpClient { } } - private async request( + private async request( name: string, schema: T, url: string, requestInit?: RequestInit, - options?: ZodFetchOptions> - ): Promise>> { + options?: ZodFetchOptions> + ): Promise>> { const start = performance.now(); const result = await wrapZodFetch(schema, url, requestInit, options); diff --git a/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts b/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts index e5360c6ed6b..37be07030e9 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/schemas.ts @@ -1,4 +1,5 @@ -import { z } from "zod"; +import { z } from "zod/v4"; +import { discriminatedUnion } from "../../utils/zod.js"; import { TaskRunExecutionResult } from "../../schemas/common.js"; import { MachineResources, @@ -28,7 +29,7 @@ export const WorkerApiHeartbeatResponseBody = z.object({ }); export type WorkerApiHeartbeatResponseBody = z.infer; -export const WorkerApiSuspendRunRequestBody = z.discriminatedUnion("success", [ +export const WorkerApiSuspendRunRequestBody = discriminatedUnion("success", [ z.object({ success: z.literal(true), checkpoint: CheckpointInput, @@ -51,7 +52,7 @@ export type WorkerApiContinueRunExecutionRequestBody = z.infer< >; export const WorkerApiConnectRequestBody = z.object({ - metadata: z.record(z.any()), + metadata: z.record(z.string(), z.any()), }); export type WorkerApiConnectRequestBody = z.infer; @@ -104,7 +105,7 @@ export type WorkerApiRunAttemptStartRequestBody = z.infer< export const WorkerApiRunAttemptStartResponseBody = StartRunAttemptResult.and( z.object({ - envVars: z.record(z.string()), + envVars: z.record(z.string(), z.string()), }) ); export type WorkerApiRunAttemptStartResponseBody = z.infer< diff --git a/packages/core/src/v3/runEngineWorker/workload/http.ts b/packages/core/src/v3/runEngineWorker/workload/http.ts index 02e5d80b911..b2581c11849 100644 --- a/packages/core/src/v3/runEngineWorker/workload/http.ts +++ b/packages/core/src/v3/runEngineWorker/workload/http.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import type { WorkloadHeartbeatRequestBody, WorkloadRunAttemptCompleteRequestBody, diff --git a/packages/core/src/v3/runEngineWorker/workload/schemas.ts b/packages/core/src/v3/runEngineWorker/workload/schemas.ts index 14f3b8efe35..1abe9214de5 100644 --- a/packages/core/src/v3/runEngineWorker/workload/schemas.ts +++ b/packages/core/src/v3/runEngineWorker/workload/schemas.ts @@ -1,4 +1,5 @@ -import { z } from "zod"; +import { z } from "zod/v4"; +import { discriminatedUnion } from "../../utils/zod.js"; import { WorkerApiRunHeartbeatRequestBody, WorkerApiHeartbeatResponseBody, @@ -19,7 +20,7 @@ export type WorkloadHeartbeatRequestBody = z.infer; -export const WorkloadSuspendRunResponseBody = z.discriminatedUnion("ok", [ +export const WorkloadSuspendRunResponseBody = discriminatedUnion("ok", [ z.object({ ok: z.literal(true), }), diff --git a/packages/core/src/v3/schemas/api-type.test.ts b/packages/core/src/v3/schemas/api-type.test.ts index 39fb3131ce1..73c697969cd 100644 --- a/packages/core/src/v3/schemas/api-type.test.ts +++ b/packages/core/src/v3/schemas/api-type.test.ts @@ -141,6 +141,14 @@ describe("InitializeDeploymentRequestBody", () => { }); describe("TriggerTaskRequestBody", () => { + it("accepts a missing payload", () => { + const result = TriggerTaskRequestBody.safeParse({ + options: {}, + }); + + expect(result.success).toBe(true); + }); + it("accepts application/store payload as a non-empty string", () => { const result = TriggerTaskRequestBody.safeParse({ payload: "packets/payloads/file.json", diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts index 3f6ba0aeced..31902a6b15f 100644 --- a/packages/core/src/v3/schemas/api.ts +++ b/packages/core/src/v3/schemas/api.ts @@ -1,4 +1,5 @@ -import { z } from "zod"; +import { z } from "zod/v4"; +import { discriminatedUnion } from "../utils/zod.js"; import { DeserializedJsonSchema } from "../../schemas/json.js"; import { FlushedRunMetadata, @@ -202,8 +203,8 @@ const ConcurrencyKeySchema = z.union([z.string(), z.number()]).transform((value) export const TriggerTaskRequestBody = z .object({ - payload: z.any(), - context: z.any(), + payload: z.any().optional(), + context: z.any().optional(), options: z .object({ /** @deprecated engine v1 only */ @@ -254,7 +255,7 @@ export const TriggerTaskRequestBody = z machine: MachinePresetName.optional(), maxAttempts: z.number().int().optional(), maxDuration: z.number().optional(), - metadata: z.any(), + metadata: z.any().optional(), metadataType: z.string().optional(), payloadType: z.string().optional(), /** @@ -312,8 +313,8 @@ export type BatchTriggerTaskRequestBody = z.infer; * * `options` reuses the strict shape from BatchTriggerTaskItem so that the * Phase-2 streaming path validates option fields identically to the V2/V3 - * batch trigger endpoints — historically this used z.record(z.unknown()) and + * batch trigger endpoints — historically this used z.record(z.string(), z.unknown()) and * let invalid values (e.g. numeric concurrencyKey) reach Prisma. */ export const BatchItemNDJSON = z.object({ @@ -544,7 +545,7 @@ export const RescheduleRunRequestBody = z.object({ export type RescheduleRunRequestBody = z.infer; export const GetEnvironmentVariablesResponseBody = z.object({ - variables: z.record(z.string()), + variables: z.record(z.string(), z.string()), }); export type GetEnvironmentVariablesResponseBody = z.infer< @@ -658,7 +659,7 @@ export type CreateArtifactRequestBody = z.infer (body.filter ? 1 : 0) + (body.runIds ? 1 : 0) === 1, { - message: "Exactly one of filter or runIds must be provided", - }); +export const CreateBulkActionRequestBody = discriminatedUnion("action", [ + z.object({ + action: z.literal("cancel"), + targetRegion: z.never().optional(), + ...BulkActionSelectionRequestBody, + }), + z.object({ + action: z.literal("replay"), + targetRegion: z.string().optional(), + ...BulkActionSelectionRequestBody, + }), +]).refine((body) => (body.filter ? 1 : 0) + (body.runIds ? 1 : 0) === 1, { + message: "Exactly one of filter or runIds must be provided", +}); export type CreateBulkActionRequestBody = z.infer; @@ -1385,17 +1384,15 @@ export type UpdateEnvironmentVariableRequestBody = z.infer< >; export const ImportEnvironmentVariablesRequestBody = z.object({ - variables: z.record(z.string()), - parentVariables: z.record(z.string()).optional(), + variables: z.record(z.string(), z.string()), + parentVariables: z.record(z.string(), z.string()).optional(), override: z.boolean().optional(), // When omitted, variables default to non-secret (the DB default is false). isSecret: z.boolean().optional(), - source: z - .discriminatedUnion("type", [ - z.object({ type: z.literal("user"), userId: z.string() }), - z.object({ type: z.literal("integration"), integration: z.string() }), - ]) - .optional(), + source: discriminatedUnion("type", [ + z.object({ type: z.literal("user"), userId: z.string() }), + z.object({ type: z.literal("integration"), integration: z.string() }), + ]).optional(), }); export type ImportEnvironmentVariablesRequestBody = z.infer< @@ -1443,7 +1440,7 @@ export const UpdateMetadataRequestBody = FlushedRunMetadata; export type UpdateMetadataRequestBody = z.infer; export const UpdateMetadataResponseBody = z.object({ - metadata: z.record(DeserializedJsonSchema), + metadata: z.record(z.string(), DeserializedJsonSchema), }); export type UpdateMetadataResponseBody = z.infer; @@ -1715,7 +1712,7 @@ export type CompleteWaitpointTokenRequestBody = z.infer; export const ResolvePromptRequestBody = z.object({ - variables: z.record(z.unknown()).default({}), + variables: z.record(z.string(), z.unknown()).default({}), label: z.string().optional(), version: z.number().optional(), }); @@ -2225,7 +2222,7 @@ export const ResolvePromptResponseBody = z.object({ template: z.string().optional(), text: z.string().optional(), model: z.string().optional().nullable(), - config: z.record(z.unknown()).optional().nullable(), + config: z.record(z.string(), z.unknown()).optional().nullable(), }), }); export type ResolvePromptResponseBody = z.infer; diff --git a/packages/core/src/v3/schemas/batchItemNDJSON.test.ts b/packages/core/src/v3/schemas/batchItemNDJSON.test.ts index f130bba4450..3eb7004c197 100644 --- a/packages/core/src/v3/schemas/batchItemNDJSON.test.ts +++ b/packages/core/src/v3/schemas/batchItemNDJSON.test.ts @@ -58,6 +58,15 @@ describe("concurrencyKey coercion", () => { }); describe("BatchTriggerTaskItem", () => { + it("accepts a missing payload", () => { + const result = BatchTriggerTaskItem.safeParse({ + task: "user-workflow-tick", + options: {}, + }); + + expect(result.success).toBe(true); + }); + it("coerces a numeric concurrencyKey to a string", () => { const result = BatchTriggerTaskItem.safeParse({ task: "user-workflow-tick", diff --git a/packages/core/src/v3/schemas/build.ts b/packages/core/src/v3/schemas/build.ts index 52cae0b2385..a0a0330a09a 100644 --- a/packages/core/src/v3/schemas/build.ts +++ b/packages/core/src/v3/schemas/build.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { ConfigManifest } from "./config.js"; import { PromptManifest, QueueManifest, SkillManifest, TaskFile, TaskManifest } from "./schemas.js"; @@ -41,6 +41,7 @@ export const BuildManifest = z.object({ config: ConfigManifest, files: z.array(TaskFile), sources: z.record( + z.string(), z.object({ contents: z.string(), contentHash: z.string(), @@ -56,18 +57,18 @@ export const BuildManifest = z.object({ configPath: z.string(), externals: BuildExternal.array().optional(), build: z.object({ - env: z.record(z.string()).optional(), + env: z.record(z.string(), z.string()).optional(), commands: z.array(z.string()).optional(), }), customConditions: z.array(z.string()).optional(), deploy: z.object({ - env: z.record(z.string()).optional(), + env: z.record(z.string(), z.string()).optional(), sync: z .object({ - env: z.record(z.string()).optional(), - parentEnv: z.record(z.string()).optional(), - secretEnv: z.record(z.string()).optional(), - secretParentEnv: z.record(z.string()).optional(), + env: z.record(z.string(), z.string()).optional(), + parentEnv: z.record(z.string(), z.string()).optional(), + secretEnv: z.record(z.string(), z.string()).optional(), + secretParentEnv: z.record(z.string(), z.string()).optional(), }) .optional(), }), @@ -84,7 +85,7 @@ export const BuildManifest = z.object({ }) .optional(), /** Maps output file paths to their content hashes for deduplication during dev */ - outputHashes: z.record(z.string()).optional(), + outputHashes: z.record(z.string(), z.string()).optional(), /** Skills discovered and bundled into `.trigger/skills/{id}/` under `outputPath`. */ skills: SkillManifest.array().optional(), }); @@ -113,7 +114,7 @@ export const WorkerManifest = z.object({ runtime: BuildRuntime, runtimeVersion: z.string().optional(), customConditions: z.array(z.string()).optional(), - timings: z.record(z.number()).optional(), + timings: z.record(z.string(), z.number()).optional(), processKeepAlive: z .object({ enabled: z.boolean(), diff --git a/packages/core/src/v3/schemas/checkpoints.ts b/packages/core/src/v3/schemas/checkpoints.ts index f11698f2df7..1f8b032966b 100644 --- a/packages/core/src/v3/schemas/checkpoints.ts +++ b/packages/core/src/v3/schemas/checkpoints.ts @@ -1,5 +1,5 @@ import { CheckpointType, DequeuedMessage } from "./runEngine.js"; -import z from "zod"; +import z from "zod/v4"; const _CallbackUrl = z .string() diff --git a/packages/core/src/v3/schemas/common.ts b/packages/core/src/v3/schemas/common.ts index a6ec4b1002d..8574dbf33bd 100644 --- a/packages/core/src/v3/schemas/common.ts +++ b/packages/core/src/v3/schemas/common.ts @@ -1,4 +1,5 @@ -import { z } from "zod"; +import { z } from "zod/v4"; +import { discriminatedUnion } from "../utils/zod.js"; import { DeserializedJsonSchema } from "../../schemas/json.js"; import type { RuntimeEnvironmentType as DBRuntimeEnvironmentType } from "@trigger.dev/database"; @@ -25,7 +26,7 @@ const MetadataOperationKey = z.string().refine(isSafeMetadataKey, { export const RunMetadataUpdateOperation = z.object({ type: z.literal("update"), - value: z.record(z.unknown()), + value: z.record(z.string(), z.unknown()), }); export type RunMetadataUpdateOperation = z.infer; @@ -69,7 +70,7 @@ export const RunMetadataIncrementKeyOperation = z.object({ export type RunMetadataIncrementKeyOperation = z.infer; -export const RunMetadataChangeOperation = z.discriminatedUnion("type", [ +export const RunMetadataChangeOperation = discriminatedUnion("type", [ RunMetadataUpdateOperation, RunMetadataSetKeyOperation, RunMetadataDeleteKeyOperation, @@ -81,7 +82,7 @@ export const RunMetadataChangeOperation = z.discriminatedUnion("type", [ export type RunMetadataChangeOperation = z.infer; export const FlushedRunMetadata = z.object({ - metadata: z.record(DeserializedJsonSchema).optional(), + metadata: z.record(z.string(), DeserializedJsonSchema).optional(), operations: z.array(RunMetadataChangeOperation).optional(), parentOperations: z.array(RunMetadataChangeOperation).optional(), rootOperations: z.array(RunMetadataChangeOperation).optional(), @@ -220,7 +221,7 @@ export type TaskRunInternalError = z.infer; export const TaskRunErrorCodes = TaskRunInternalError.shape.code.enum; export type TaskRunErrorCodes = TaskRunInternalError["code"]; -export const TaskRunError = z.discriminatedUnion("type", [ +export const TaskRunError = discriminatedUnion("type", [ TaskRunBuiltInError, TaskRunCustomErrorObject, TaskRunStringError, @@ -244,7 +245,7 @@ export const TaskRun = z.object({ idempotencyKeyScope: z.enum(["run", "attempt", "global"]).optional(), maxAttempts: z.number().optional(), version: z.string().optional(), - metadata: z.record(DeserializedJsonSchema).optional(), + metadata: z.record(z.string(), DeserializedJsonSchema).optional(), maxDuration: z.number().optional(), /** The priority of the run. Wih a value of 10 it will be dequeued before runs that were triggered 9 seconds before it (assuming they had no priority set). */ priority: z.number().optional(), @@ -365,7 +366,7 @@ export const TaskRunExecution = z.object({ attempt: TaskRunExecutionAttempt.passthrough(), run: TaskRun.and( z.object({ - traceContext: z.record(z.unknown()).optional(), + traceContext: z.record(z.string(), z.unknown()).optional(), realtimeStreamsVersion: z.string().optional(), }) ), @@ -408,7 +409,7 @@ export const V3TaskRun = z.object({ idempotencyKeyScope: z.enum(["run", "attempt", "global"]).optional(), maxAttempts: z.number().optional(), version: z.string().optional(), - metadata: z.record(DeserializedJsonSchema).optional(), + metadata: z.record(z.string(), DeserializedJsonSchema).optional(), maxDuration: z.number().optional(), context: z.unknown(), durationMs: z.number(), @@ -423,7 +424,7 @@ export const V3TaskRunExecution = z.object({ attempt: V3TaskRunExecutionAttempt, run: V3TaskRun.and( z.object({ - traceContext: z.record(z.unknown()).optional(), + traceContext: z.record(z.string(), z.unknown()).optional(), }) ), queue: TaskRunExecutionQueue, @@ -537,7 +538,7 @@ export const TaskRunSuccessfulExecutionResult = z.object({ export type TaskRunSuccessfulExecutionResult = z.infer; -export const TaskRunExecutionResult = z.discriminatedUnion("ok", [ +export const TaskRunExecutionResult = discriminatedUnion("ok", [ TaskRunSuccessfulExecutionResult, TaskRunFailedExecutionResult, ]); diff --git a/packages/core/src/v3/schemas/config.ts b/packages/core/src/v3/schemas/config.ts index f94098f2802..fa7570125f2 100644 --- a/packages/core/src/v3/schemas/config.ts +++ b/packages/core/src/v3/schemas/config.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; export const ConfigManifest = z.object({ project: z.string(), diff --git a/packages/core/src/v3/schemas/errors.ts b/packages/core/src/v3/schemas/errors.ts index 192da5b6de7..9862d25f93e 100644 --- a/packages/core/src/v3/schemas/errors.ts +++ b/packages/core/src/v3/schemas/errors.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; /** * The lifecycle state of an error group. Mirrors the dashboard's diff --git a/packages/core/src/v3/schemas/eventFilter.ts b/packages/core/src/v3/schemas/eventFilter.ts index 6ddda362108..11d3b8753dc 100644 --- a/packages/core/src/v3/schemas/eventFilter.ts +++ b/packages/core/src/v3/schemas/eventFilter.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; export const stringPatternMatchers = [ z.object({ @@ -65,5 +65,5 @@ type EventMatcher = z.infer; export type EventFilter = { [key: string]: EventMatcher | EventFilter }; export const EventFilter: z.ZodType = z.lazy(() => - z.record(z.union([EventMatcher, EventFilter])) + z.record(z.string(), z.union([EventMatcher, EventFilter])) ); diff --git a/packages/core/src/v3/schemas/fetch.ts b/packages/core/src/v3/schemas/fetch.ts index 88ccad4e357..edf5f581aac 100644 --- a/packages/core/src/v3/schemas/fetch.ts +++ b/packages/core/src/v3/schemas/fetch.ts @@ -1,4 +1,5 @@ -import { z } from "zod"; +import { z } from "zod/v4"; +import { discriminatedUnion } from "../utils/zod.js"; import { RetryOptions } from "./schemas.js"; import { EventFilter } from "./eventFilter.js"; import type { Prettify } from "../types/index.js"; @@ -39,7 +40,7 @@ export const FetchRetryBackoffStrategy = RetryOptions.extend({ /** The `backoff` strategy retries the request with an exponential backoff. */ export type FetchRetryBackoffStrategy = z.infer; -export const FetchRetryStrategy = z.discriminatedUnion("strategy", [ +export const FetchRetryStrategy = discriminatedUnion("strategy", [ FetchRetryHeadersStrategy, FetchRetryBackoffStrategy, ]); diff --git a/packages/core/src/v3/schemas/messages.ts b/packages/core/src/v3/schemas/messages.ts index 1389fcddc40..d70a032b479 100644 --- a/packages/core/src/v3/schemas/messages.ts +++ b/packages/core/src/v3/schemas/messages.ts @@ -1,9 +1,10 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { ImportTaskFileErrors, WorkerManifest } from "./build.js"; import { TaskRunExecution, TaskRunExecutionResult } from "./common.js"; import { RunEngineVersionSchema, TaskRunExecutionMetrics } from "./schemas.js"; import { CompletedWaitpoint } from "./runEngine.js"; import { DebugLogPropertiesInput } from "../runEngineWorker/supervisor/schemas.js"; +import type { ZodIssueLike } from "../types/schemas.js"; export const ServerBackgroundWorker = z.object({ id: z.string(), @@ -27,7 +28,7 @@ export const UncaughtExceptionMessage = z.object({ export const TaskMetadataFailedToParseData = z.object({ version: z.literal("v1").default("v1"), tasks: z.unknown(), - zodIssues: z.custom((v) => { + zodIssues: z.custom>((v) => { return Array.isArray(v) && v.every((issue) => typeof issue === "object" && "message" in issue); }), }); @@ -90,10 +91,10 @@ export const WorkerToExecutorMessageCatalog = { message: z.object({ version: z.literal("v1").default("v1"), execution: TaskRunExecution, - traceContext: z.record(z.unknown()), + traceContext: z.record(z.string(), z.unknown()), metadata: ServerBackgroundWorker, metrics: TaskRunExecutionMetrics.optional(), - env: z.record(z.string()).optional(), + env: z.record(z.string(), z.string()).optional(), isWarmStart: z.boolean().optional(), }), }, diff --git a/packages/core/src/v3/schemas/openTelemetry.ts b/packages/core/src/v3/schemas/openTelemetry.ts index 47d9d9a50d9..2a21f9ad894 100644 --- a/packages/core/src/v3/schemas/openTelemetry.ts +++ b/packages/core/src/v3/schemas/openTelemetry.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; export const ExceptionEventProperties = z.object({ type: z.string().optional(), @@ -43,7 +43,7 @@ export type AttemptFailedSpanEvent = z.infer; export const OtherSpanEvent = z.object({ name: z.string(), time: z.coerce.date(), - properties: z.record(z.unknown()), + properties: z.record(z.string(), z.unknown()), }); export type OtherSpanEvent = z.infer; diff --git a/packages/core/src/v3/schemas/query.ts b/packages/core/src/v3/schemas/query.ts index 027259e8b74..3735ddfdff2 100644 --- a/packages/core/src/v3/schemas/query.ts +++ b/packages/core/src/v3/schemas/query.ts @@ -1,4 +1,5 @@ -import { z } from "zod"; +import { z } from "zod/v4"; +import { discriminatedUnion } from "../utils/zod.js"; import type { IdempotencyKeyScope } from "../idempotency-key-catalog/catalog.js"; import type { MachinePresetName, RuntimeEnvironmentType } from "./common.js"; @@ -21,7 +22,7 @@ export type QueryExecuteRequestBody = z.infer; */ export const QueryExecuteJSONResponseBody = z.object({ format: z.literal("json"), - results: z.array(z.record(z.any())), + results: z.array(z.record(z.string(), z.any())), }); export type QueryExecuteJSONResponseBody = z.infer; @@ -36,7 +37,7 @@ export const QueryExecuteCSVResponseBody = z.object({ export type QueryExecuteCSVResponseBody = z.infer; -export const QueryExecuteResponseBody = z.discriminatedUnion("format", [ +export const QueryExecuteResponseBody = discriminatedUnion("format", [ QueryExecuteJSONResponseBody, QueryExecuteCSVResponseBody, ]); diff --git a/packages/core/src/v3/schemas/queues.ts b/packages/core/src/v3/schemas/queues.ts index 34a47b34e3e..78d310174dd 100644 --- a/packages/core/src/v3/schemas/queues.ts +++ b/packages/core/src/v3/schemas/queues.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; const queueTypes = ["task", "custom"] as const; diff --git a/packages/core/src/v3/schemas/resources.ts b/packages/core/src/v3/schemas/resources.ts index 753324d1257..8dd89aa974b 100644 --- a/packages/core/src/v3/schemas/resources.ts +++ b/packages/core/src/v3/schemas/resources.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { QueueManifest, RetryOptions, ScheduleMetadata } from "./schemas.js"; import { MachineConfig } from "./common.js"; @@ -46,7 +46,7 @@ export const PromptResource = z.object({ /** Default model identifier */ model: z.string().optional(), /** Default model config (temperature, maxTokens, etc.) */ - config: z.record(z.unknown()).optional(), + config: z.record(z.string(), z.unknown()).optional(), /** JSONSchema7 for template variables */ variableSchema: z.unknown().optional(), }); diff --git a/packages/core/src/v3/schemas/runEngine.ts b/packages/core/src/v3/schemas/runEngine.ts index 92144364494..b96d823eb67 100644 --- a/packages/core/src/v3/schemas/runEngine.ts +++ b/packages/core/src/v3/schemas/runEngine.ts @@ -1,4 +1,5 @@ -import { z } from "zod"; +import { z } from "zod/v4"; +import { discriminatedUnion } from "../utils/zod.js"; import type { Enum, RuntimeEnvironmentType } from "./common.js"; import { MachinePreset, TaskRunExecution } from "./common.js"; import { EnvironmentType } from "./schemas.js"; @@ -223,7 +224,7 @@ export const RunExecutionData = z.object({ }); export type RunExecutionData = z.infer; -export const CreateCheckpointResult = z.discriminatedUnion("ok", [ +export const CreateCheckpointResult = discriminatedUnion("ok", [ z .object({ ok: z.literal(true), @@ -288,7 +289,7 @@ export const DequeuedMessage = z.object({ machine: MachinePreset, attemptNumber: z.number(), masterQueue: z.string(), - traceContext: z.record(z.unknown()), + traceContext: z.record(z.string(), z.unknown()), annotations: RunAnnotations.optional(), }), environment: z.object({ diff --git a/packages/core/src/v3/schemas/schemas.ts b/packages/core/src/v3/schemas/schemas.ts index 0b12e7ae3db..8d6b6b79c01 100644 --- a/packages/core/src/v3/schemas/schemas.ts +++ b/packages/core/src/v3/schemas/schemas.ts @@ -1,4 +1,5 @@ -import { z } from "zod"; +import { z } from "zod/v4"; +import { discriminatedUnion } from "../utils/zod.js"; import type { RequireKeys } from "../types/index.js"; import { MachineConfig, @@ -31,8 +32,8 @@ export type TaskRunExecutionMetrics = z.infer; export const TaskRunExecutionPayload = z.object({ execution: TaskRunExecution, - traceContext: z.record(z.unknown()), - environment: z.record(z.string()).optional(), + traceContext: z.record(z.string(), z.unknown()), + environment: z.record(z.string(), z.string()).optional(), metrics: TaskRunExecutionMetrics.optional(), }); @@ -56,8 +57,8 @@ export type V3ProdTaskRunExecution = z.infer; export const V3ProdTaskRunExecutionPayload = z.object({ execution: V3ProdTaskRunExecution, - traceContext: z.record(z.unknown()), - environment: z.record(z.string()).optional(), + traceContext: z.record(z.string(), z.unknown()), + environment: z.record(z.string(), z.string()).optional(), metrics: TaskRunExecutionMetrics.optional(), }); @@ -95,7 +96,7 @@ export const SlidingWindowRateLimit = z.object({ ]), }); -export const RateLimitOptions = z.discriminatedUnion("type", [ +export const RateLimitOptions = discriminatedUnion("type", [ FixedWindowRateLimit, SlidingWindowRateLimit, ]); @@ -231,7 +232,7 @@ const promptMetadata = { description: z.string().optional(), content: z.string().optional(), model: z.string().optional(), - config: z.record(z.unknown()).optional(), + config: z.record(z.string(), z.unknown()).optional(), variableSchema: z.unknown().optional(), }; @@ -320,8 +321,8 @@ export const TaskRunExecutionLazyAttemptPayload = z.object({ messageId: z.string(), isTest: z.boolean(), isReplay: z.boolean().default(false), - traceContext: z.record(z.unknown()), - environment: z.record(z.string()).optional(), + traceContext: z.record(z.string(), z.unknown()), + environment: z.record(z.string(), z.string()).optional(), metrics: TaskRunExecutionMetrics.optional(), }); diff --git a/packages/core/src/v3/schemas/style.ts b/packages/core/src/v3/schemas/style.ts index 2f833b800ac..b34b7406fbb 100644 --- a/packages/core/src/v3/schemas/style.ts +++ b/packages/core/src/v3/schemas/style.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; export const PRIMARY_VARIANT = "primary"; export const WARM_VARIANT = "warm"; diff --git a/packages/core/src/v3/schemas/tokens.ts b/packages/core/src/v3/schemas/tokens.ts index ba01f240cdf..c1cd4914b1d 100644 --- a/packages/core/src/v3/schemas/tokens.ts +++ b/packages/core/src/v3/schemas/tokens.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; export const CreateAuthorizationCodeResponseSchema = z.object({ url: z.string().url(), diff --git a/packages/core/src/v3/schemas/warmStart.ts b/packages/core/src/v3/schemas/warmStart.ts index 1d0e43268e4..56b7a09cf20 100644 --- a/packages/core/src/v3/schemas/warmStart.ts +++ b/packages/core/src/v3/schemas/warmStart.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; export const WarmStartConnectResponse = z.object({ connectionTimeoutMs: z.number().optional(), diff --git a/packages/core/src/v3/schemas/webhooks.ts b/packages/core/src/v3/schemas/webhooks.ts index b5ed927602e..98b99f86f8a 100644 --- a/packages/core/src/v3/schemas/webhooks.ts +++ b/packages/core/src/v3/schemas/webhooks.ts @@ -1,4 +1,5 @@ -import { z } from "zod"; +import { z } from "zod/v4"; +import { discriminatedUnion } from "../utils/zod.js"; import { RunStatus } from "./api.js"; import { RuntimeEnvironmentTypeSchema, TaskRunError } from "./common.js"; @@ -257,7 +258,7 @@ const commonProperties = { }; /** Represents all possible webhook types */ -export const Webhook = z.discriminatedUnion("type", [ +export const Webhook = discriminatedUnion("type", [ /** Run failed alert webhook */ z.object({ ...commonProperties, diff --git a/packages/core/src/v3/serverOnly/httpServer.ts b/packages/core/src/v3/serverOnly/httpServer.ts index 8360067d583..c6b26785068 100644 --- a/packages/core/src/v3/serverOnly/httpServer.ts +++ b/packages/core/src/v3/serverOnly/httpServer.ts @@ -1,30 +1,31 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; -import { z } from "zod"; +import { z } from "zod/v4"; import { SimpleStructuredLogger } from "../utils/structuredLogger.js"; import { HttpReply, getJsonBody } from "../apps/http.js"; import type { Registry } from "prom-client"; import { Histogram, Counter } from "prom-client"; import { tryCatch } from "../../utils.js"; +import type { AnyZodSchema, inferZodSchemaOutput } from "../types/schemas.js"; const logger = new SimpleStructuredLogger("http-server"); type RouteHandler< - TParams extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, - TQuery extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, - TBody extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, + TParams extends AnyZodSchema = z.ZodUnknown, + TQuery extends AnyZodSchema = z.ZodUnknown, + TBody extends AnyZodSchema = z.ZodUnknown, > = (ctx: { - params: z.infer; - queryParams: z.infer; - body: z.infer; + params: inferZodSchemaOutput; + queryParams: inferZodSchemaOutput; + body: inferZodSchemaOutput; req: IncomingMessage; res: ServerResponse; reply: HttpReply; }) => Promise; interface RouteDefinition< - TParams extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, - TQuery extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, - TBody extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, + TParams extends AnyZodSchema = z.ZodUnknown, + TQuery extends AnyZodSchema = z.ZodUnknown, + TBody extends AnyZodSchema = z.ZodUnknown, > { paramsSchema?: TParams; querySchema?: TQuery; @@ -235,9 +236,9 @@ export class HttpServer { } route< - TParams extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, - TQuery extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, - TBody extends z.ZodFirstPartySchemaTypes = z.ZodUnknown, + TParams extends AnyZodSchema = z.ZodUnknown, + TQuery extends AnyZodSchema = z.ZodUnknown, + TBody extends AnyZodSchema = z.ZodUnknown, >(path: `/${string}`, method: HttpMethod, definition: RouteDefinition) { this.routes[path] = { ...this.routes[path], @@ -284,8 +285,8 @@ export class HttpServer { } private optionalSchema< - TSchema extends z.ZodFirstPartySchemaTypes | undefined, - TData extends TSchema extends z.ZodFirstPartySchemaTypes ? z.TypeOf : TData, + TSchema extends AnyZodSchema | undefined, + TData extends TSchema extends AnyZodSchema ? inferZodSchemaOutput : TData, >( schema: TSchema, data: TData @@ -296,7 +297,7 @@ export class HttpServer { } | { success: true; - data: TSchema extends z.ZodFirstPartySchemaTypes ? z.infer : TData; + data: TSchema extends AnyZodSchema ? inferZodSchemaOutput : TData; } { if (!schema) { return { success: true, data }; @@ -308,7 +309,10 @@ export class HttpServer { return { success: false, error: parsed.error.message }; } - return { success: true, data: parsed.data }; + return { + success: true, + data: parsed.data as TSchema extends AnyZodSchema ? inferZodSchemaOutput : TData, + }; } private parseQueryParams(url: string): Record { diff --git a/packages/core/src/v3/sessionStreams/chatSnapshot.ts b/packages/core/src/v3/sessionStreams/chatSnapshot.ts index 84b204a134d..565a00a57ae 100644 --- a/packages/core/src/v3/sessionStreams/chatSnapshot.ts +++ b/packages/core/src/v3/sessionStreams/chatSnapshot.ts @@ -17,7 +17,7 @@ * recognise a version silently fall back to no-snapshot behaviour. */ -import { z } from "zod"; +import { z } from "zod/v4"; import type { UIMessage } from "ai"; diff --git a/packages/core/src/v3/types/schemas.test.ts b/packages/core/src/v3/types/schemas.test.ts new file mode 100644 index 00000000000..764c1d89ebf --- /dev/null +++ b/packages/core/src/v3/types/schemas.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; +import { z as z3 } from "zod/v3"; +import { z as z4 } from "zod/v4"; +import { getSchemaParseFn, isSchemaZodEsque } from "./schemas.js"; +import { convertToolParametersToSchema, type inferToolParameters } from "./tools.js"; + +describe("Zod schema compatibility", () => { + it("detects and parses Zod 3 schemas", async () => { + const schema = z3.object({ name: z3.string() }); + + expect(isSchemaZodEsque(schema)).toBe(true); + expect(convertToolParametersToSchema(schema)).toBe(schema); + await expect(getSchemaParseFn(schema)({ name: "Trigger.dev" })).resolves.toEqual({ + name: "Trigger.dev", + }); + + expectTypeOf>().toEqualTypeOf<{ name: string }>(); + }); + + it("detects and parses Zod 4 schemas", async () => { + const schema = z4.object({ name: z4.string() }); + + expect(isSchemaZodEsque(schema)).toBe(true); + expect(convertToolParametersToSchema(schema)).toBe(schema); + await expect(getSchemaParseFn(schema)({ name: "Trigger.dev" })).resolves.toEqual({ + name: "Trigger.dev", + }); + + expectTypeOf>().toEqualTypeOf<{ name: string }>(); + }); + + it("does not identify non-schemas as Zod schemas", () => { + expect(isSchemaZodEsque(null)).toBe(false); + expect(isSchemaZodEsque({})).toBe(false); + }); +}); diff --git a/packages/core/src/v3/types/schemas.ts b/packages/core/src/v3/types/schemas.ts index b4121029925..e90ab2cd11f 100644 --- a/packages/core/src/v3/types/schemas.ts +++ b/packages/core/src/v3/types/schemas.ts @@ -1,14 +1,34 @@ export type SchemaZodEsque = { _input: TInput; _output: TParsedInput; + parse: (input: unknown) => TParsedInput; + parseAsync: (input: unknown) => Promise; + safeParse: ( + input: unknown + ) => { success: true; data: TParsedInput } | { success: false; error: any }; +}; + +export type AnyZodSchema = SchemaZodEsque; +export type inferZodSchemaInput = TSchema["_input"]; +export type inferZodSchemaOutput = TSchema["_output"]; + +export type ZodIssueLike = { + message: string; + path: ReadonlyArray; +}; + +export type ZodErrorLike = { + message: string; + issues: ReadonlyArray; }; export function isSchemaZodEsque( - schema: Schema + schema: unknown ): schema is SchemaZodEsque { return ( typeof schema === "object" && - "_def" in schema && + schema !== null && + ("_zod" in schema || "_def" in schema) && "parse" in schema && "parseAsync" in schema && "safeParse" in schema diff --git a/packages/core/src/v3/types/tools.ts b/packages/core/src/v3/types/tools.ts index 9df741dc0ad..52fcf6f3c99 100644 --- a/packages/core/src/v3/types/tools.ts +++ b/packages/core/src/v3/types/tools.ts @@ -1,22 +1,26 @@ -import { z } from "zod"; import type { Schema as AISchema } from "ai"; -import type { Schema } from "./schemas.js"; +import { + isSchemaZodEsque, + type AnyZodSchema, + type inferZodSchemaOutput, + type Schema, +} from "./schemas.js"; -export type ToolTaskParameters = z.ZodTypeAny | AISchema; +export type ToolTaskParameters = AnyZodSchema | AISchema; export type inferToolParameters = PARAMETERS extends AISchema ? PARAMETERS["_type"] - : PARAMETERS extends z.ZodTypeAny - ? z.infer + : PARAMETERS extends AnyZodSchema + ? inferZodSchemaOutput : never; export function convertToolParametersToSchema( toolParameters: TToolParameters ): Schema { - return toolParameters instanceof z.ZodSchema + return isSchemaZodEsque(toolParameters) ? toolParameters - : convertAISchemaToTaskSchema(toolParameters); + : convertAISchemaToTaskSchema(toolParameters as AISchema); } function convertAISchemaToTaskSchema(schema: AISchema): Schema { diff --git a/packages/core/src/v3/utils/ioSerialization.ts b/packages/core/src/v3/utils/ioSerialization.ts index 5502afb0981..a63a4397303 100644 --- a/packages/core/src/v3/utils/ioSerialization.ts +++ b/packages/core/src/v3/utils/ioSerialization.ts @@ -1,6 +1,6 @@ import { JSONHeroPath } from "@jsonhero/path"; import type { Attributes, Span } from "@opentelemetry/api"; -import { z } from "zod"; +import { z } from "zod/v4"; import type { ApiClient } from "../apiClient/index.js"; import { apiClientManager } from "../apiClientManager-api.js"; import { diff --git a/packages/core/src/v3/utils/zod.ts b/packages/core/src/v3/utils/zod.ts new file mode 100644 index 00000000000..8f35be3e181 --- /dev/null +++ b/packages/core/src/v3/utils/zod.ts @@ -0,0 +1,20 @@ +import { z } from "zod/v4"; + +/** + * Preserve the Zod 4.0-compatible declaration shape while using newer Zod 4 + * implementations at runtime. Newer releases infer the discriminator as a + * second type parameter, which older `zod/v4` permalinks cannot consume. + */ +export type CompatibleZodDiscriminatedUnion< + Types extends readonly [z.ZodTypeAny, ...z.ZodTypeAny[]], +> = z.ZodDiscriminatedUnion; + +export function discriminatedUnion( + discriminator: string, + options: Types +): CompatibleZodDiscriminatedUnion { + return z.discriminatedUnion( + discriminator, + options as any + ) as CompatibleZodDiscriminatedUnion; +} diff --git a/packages/core/src/v3/workloadDeploymentToken.ts b/packages/core/src/v3/workloadDeploymentToken.ts index 4f0d367038f..0fd1fc3543e 100644 --- a/packages/core/src/v3/workloadDeploymentToken.ts +++ b/packages/core/src/v3/workloadDeploymentToken.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { generateJWT, validateJWT } from "./jwt.js"; /** diff --git a/packages/core/src/v3/zodIpc.ts b/packages/core/src/v3/zodIpc.ts index d810d255d7f..ebd5a685df7 100644 --- a/packages/core/src/v3/zodIpc.ts +++ b/packages/core/src/v3/zodIpc.ts @@ -1,10 +1,12 @@ import { randomUUID } from "crypto"; -import { z } from "zod"; +import { z } from "zod/v4"; +import { discriminatedUnion } from "./utils/zod.js"; import type { ExecutorToWorkerMessageCatalog, WorkerToExecutorMessageCatalog, } from "./schemas/messages.js"; import { ZodSchemaParsedError } from "./zodMessageHandler.js"; +import type { inferZodSchemaInput, inferZodSchemaOutput } from "./types/schemas.js"; import type { GetSocketCallbackSchema, GetSocketMessageSchema, @@ -18,13 +20,13 @@ import type { interface ZodIpcMessageSender { send>( type: K, - payload: z.input> + payload: inferZodSchemaInput> ): Promise; sendWithAck>( type: K, - payload: z.input> - ): Promise>>; + payload: inferZodSchemaInput> + ): Promise>>; } type ZodIpcMessageHandlers< @@ -32,11 +34,11 @@ type ZodIpcMessageHandlers< TEmitCatalog extends ZodSocketMessageCatalogSchema, > = Partial<{ [K in keyof TListenCatalog]: ( - payload: z.infer>, + payload: inferZodSchemaOutput>, sender: ZodIpcMessageSender ) => Promise< SocketMessageHasCallback extends true - ? z.input> + ? inferZodSchemaInput> : void >; }>; @@ -84,7 +86,8 @@ class ZodIpcMessageHandler< return; } - const ack = await handler(parsedMessage.payload, this.#sender); + // parsedMessage.payload is parsed output at runtime; zod v4 distinguishes input/output + const ack = await handler(parsedMessage.payload as Parameters[0], this.#sender); return ack; } @@ -114,7 +117,7 @@ class ZodIpcMessageHandler< } } -const Packet = z.discriminatedUnion("type", [ +const Packet = discriminatedUnion("type", [ z.object({ type: z.literal("CONNECT"), sessionId: z.string().optional(), @@ -274,7 +277,7 @@ export class ZodIpcConnection< async send>( type: K, - payload: z.input> + payload: inferZodSchemaInput> ): Promise { const schema = this.opts.emitSchema[type]?.["message"]; @@ -300,9 +303,9 @@ export class ZodIpcConnection< public async sendWithAck>( type: K, - payload: z.input>, + payload: inferZodSchemaInput>, timeoutInMs?: number - ): Promise>> { + ): Promise>> { const currentId = this.#messageCounter++; return new Promise((resolve, reject) => { @@ -320,7 +323,7 @@ export class ZodIpcConnection< ); }, timeoutInMs ?? defaultTimeoutInMs); - this.#acks.set(currentId, { resolve, reject, timeout }); + this.#acks.set(currentId, { resolve: resolve as (value: unknown) => void, reject, timeout }); const schema = this.opts.emitSchema[type]?.["message"]; diff --git a/packages/core/src/v3/zodMessageHandler.test.ts b/packages/core/src/v3/zodMessageHandler.test.ts new file mode 100644 index 00000000000..7f9db90e5b6 --- /dev/null +++ b/packages/core/src/v3/zodMessageHandler.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from "vitest"; +import { z as z3 } from "zod/v3"; +import { z as z4 } from "zod/v4"; +import { ZodMessageSender, sendMessageInCatalog } from "./zodMessageHandler.js"; + +const schemas = [ + [ + "Zod 3", + z3.object({ value: z3.string().transform(Number), added: z3.string().default("default") }), + ], + [ + "Zod 4", + z4.object({ value: z4.string().transform(Number), added: z4.string().default("default") }), + ], +] as const; + +describe("Zod message sending", () => { + it.each(schemas)("ZodMessageSender preserves the original %s payload", async (_name, schema) => { + const catalog = { TEST: schema }; + const payload = { value: "42", extra: "preserved" }; + const sender = vi.fn(async (_message: unknown) => {}); + const messageSender = new ZodMessageSender({ schema: catalog, sender }); + + await messageSender.send("TEST", payload); + + expect(sender).toHaveBeenCalledWith({ type: "TEST", payload, version: "v1" }); + }); + + it.each(schemas)( + "sendMessageInCatalog preserves the original %s payload", + async (_name, schema) => { + const catalog = { TEST: schema }; + const payload = { value: "42", extra: "preserved" }; + const sender = vi.fn(async (_message: unknown) => {}); + + await sendMessageInCatalog(catalog, "TEST", payload, sender); + + expect(sender).toHaveBeenCalledWith({ type: "TEST", payload, version: "v1" }); + } + ); +}); diff --git a/packages/core/src/v3/zodMessageHandler.ts b/packages/core/src/v3/zodMessageHandler.ts index 2836822faf5..27662d677bf 100644 --- a/packages/core/src/v3/zodMessageHandler.ts +++ b/packages/core/src/v3/zodMessageHandler.ts @@ -1,25 +1,29 @@ -import { z } from "zod"; +import { z } from "zod/v4"; +import type { + AnyZodSchema, + ZodErrorLike, + inferZodSchemaInput, + inferZodSchemaOutput, +} from "./types/schemas.js"; import type { StructuredLogger } from "./utils/structuredLogger.js"; export class ZodSchemaParsedError extends Error { constructor( - public error: z.ZodError, + public error: ZodErrorLike, public payload: unknown ) { super(error.message); } } -export type ZodMessageValueSchema> = - | z.ZodFirstPartySchemaTypes - | TDiscriminatedUnion; +export type ZodMessageValueSchema = TSchema; export interface ZodMessageCatalogSchema { - [key: string]: ZodMessageValueSchema; + [key: string]: AnyZodSchema; } export type ZodMessageHandlers = Partial<{ - [K in keyof TCatalogSchema]: (payload: z.infer) => Promise; + [K in keyof TCatalogSchema]: (payload: inferZodSchemaOutput) => Promise; }>; export type ZodMessageHandlerOptions = { @@ -33,13 +37,13 @@ export type MessageFromSchema< TMessageCatalog extends ZodMessageCatalogSchema, > = { type: K; - payload: z.input; + payload: inferZodSchemaInput; }; export type MessagePayloadFromSchema< K extends keyof TMessageCatalog, TMessageCatalog extends ZodMessageCatalogSchema, -> = z.output; +> = inferZodSchemaOutput; export type MessageFromCatalog = { [K in keyof TMessageCatalog]: MessageFromSchema; @@ -109,7 +113,8 @@ export class ZodMessageHandler }; } - const ack = await handler(parsedMessage.data.payload); + // parsedMessage.data.payload is parsed output at runtime; zod v4 distinguishes input/output + const ack = await handler(parsedMessage.data.payload as Parameters[0]); return { success: true, @@ -232,7 +237,7 @@ export function parseMessageFromCatalog = (message: { type: keyof TMessageCatalog; - payload: z.infer; + payload: inferZodSchemaOutput; version: "v1"; }) => Promise; @@ -262,7 +267,7 @@ export class ZodMessageSender { public async send( type: K, - payload: z.input + payload: inferZodSchemaInput ) { const schema = this.#schema[type]; @@ -277,7 +282,13 @@ export class ZodMessageSender { } try { - await this.#sender({ type, payload, version: "v1" }); + // Preserve the original payload on the wire for backwards compatibility. Validation is + // intentionally side-effect-free here; receivers parse the payload into the schema output. + await this.#sender({ + type, + payload: payload as unknown as inferZodSchemaOutput, + version: "v1", + }); } catch (error) { console.error("[ZodMessageSender] Failed to send message", error); } @@ -317,7 +328,7 @@ export class ZodMessageSender { export async function sendMessageInCatalog( catalog: TMessageCatalog, type: keyof TMessageCatalog, - payload: z.input, + payload: inferZodSchemaInput, sender: ZodMessageSenderCallback ) { const schema = catalog[type]; @@ -332,9 +343,15 @@ export async function sendMessageInCatalog, + version: "v1", + }); } export type MessageCatalogToSocketIoEvents = { - [K in keyof TCatalog]: (message: z.infer) => void; + [K in keyof TCatalog]: (message: inferZodSchemaOutput) => void; }; diff --git a/packages/core/src/v3/zodSocket.ts b/packages/core/src/v3/zodSocket.ts index 32488dd3551..db9afafdaab 100644 --- a/packages/core/src/v3/zodSocket.ts +++ b/packages/core/src/v3/zodSocket.ts @@ -1,7 +1,8 @@ import type { ManagerOptions, Socket, SocketOptions } from "socket.io-client"; import { io } from "socket.io-client"; -import { z } from "zod"; -import { fromZodError } from "zod-validation-error"; +import { z } from "zod/v4"; +import { fromZodError } from "zod-validation-error/v4"; +import type { inferZodSchemaInput, inferZodSchemaOutput } from "./types/schemas.js"; import type { StructuredLogger } from "./utils/structuredLogger.js"; import { LogLevel, SimpleStructuredLogger } from "./utils/structuredLogger.js"; import type { EventEmitterLike, ZodMessageValueSchema } from "./zodMessageHandler.js"; @@ -20,10 +21,10 @@ export interface ZodSocketMessageCatalogSchema { export type ZodMessageCatalogToSocketIoEvents = { [K in keyof TCatalog]: SocketMessageHasCallback extends true ? ( - message: z.infer>, - callback: (ack: z.infer>) => void + message: inferZodSchemaOutput>, + callback: (ack: inferZodSchemaOutput>) => void ) => void - : (message: z.infer>) => void; + : (message: inferZodSchemaOutput>) => void; }; export type GetSocketMessageSchema< @@ -34,7 +35,7 @@ export type GetSocketMessageSchema< export type InferSocketMessageSchema< TRPCCatalog extends ZodSocketMessageCatalogSchema, TMessageType extends keyof TRPCCatalog, -> = z.infer>; +> = inferZodSchemaOutput>; export type GetSocketCallbackSchema< TRPCCatalog extends ZodSocketMessageCatalogSchema, @@ -46,7 +47,7 @@ export type GetSocketCallbackSchema< export type InferSocketCallbackSchema< TRPCCatalog extends ZodSocketMessageCatalogSchema, TMessageType extends keyof TRPCCatalog, -> = z.infer>; +> = inferZodSchemaOutput>; export type SocketMessageHasCallback< TRPCCatalog extends ZodSocketMessageCatalogSchema, @@ -56,10 +57,10 @@ export type SocketMessageHasCallback< export type ZodSocketMessageHandlers = Partial<{ [K in keyof TCatalogSchema]: ( - payload: z.infer> + payload: inferZodSchemaOutput> ) => Promise< SocketMessageHasCallback extends true - ? z.input> + ? inferZodSchemaInput> : void >; }>; @@ -77,7 +78,7 @@ type MessageFromSocketSchema< TMessageCatalog extends ZodSocketMessageCatalogSchema, > = { type: K; - payload: z.input>; + payload: inferZodSchemaInput>; }; export type MessagesFromSocketCatalog = { @@ -128,7 +129,8 @@ export class ZodSocketMessageHandler[0]); return ack; } @@ -269,7 +271,7 @@ export class ZodSocketMessageSender>( type: K, - payload: z.input> + payload: inferZodSchemaInput> ): void { const schema = this.#schema[type]?.["message"]; @@ -294,9 +296,9 @@ export class ZodSocketMessageSender>( type: K, - payload: z.input>, + payload: inferZodSchemaInput>, timeout?: number - ): Promise>> { + ): Promise>> { const schema = this.#schema[type]?.["message"]; if (!schema) { @@ -314,7 +316,7 @@ export class ZodSocketMessageSender>; } } diff --git a/packages/redis-worker/package.json b/packages/redis-worker/package.json index 7c014c23436..ef49dfb37f5 100644 --- a/packages/redis-worker/package.json +++ b/packages/redis-worker/package.json @@ -28,7 +28,7 @@ "nanoid": "^5.0.7", "p-limit": "^6.2.0", "seedrandom": "^3.0.5", - "zod": "3.25.76", + "zod": "^3.25.76 || ^4.0.0", "cron-parser": "^4.9.0" }, "devDependencies": { @@ -53,5 +53,8 @@ "import": "./dist/index.js", "require": "./dist/index.cjs" } + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.0.0" } } diff --git a/packages/redis-worker/src/fair-queue/index.ts b/packages/redis-worker/src/fair-queue/index.ts index e24de876d85..0cc7ec659f3 100644 --- a/packages/redis-worker/src/fair-queue/index.ts +++ b/packages/redis-worker/src/fair-queue/index.ts @@ -1,9 +1,10 @@ import { createRedisClient, type Redis } from "@internal/redis"; import { SpanKind, type Span } from "@internal/tracing"; import { Logger } from "@trigger.dev/core/logger"; +import type { AnyZodSchema, inferZodSchemaOutput } from "@trigger.dev/core/v3"; import { nanoid } from "nanoid"; import { setInterval } from "node:timers/promises"; -import { type z } from "zod"; +import { type z } from "zod/v4"; import { isAbortError } from "../utils.js"; import { ConcurrencyManager } from "./concurrency.js"; import { MasterQueue } from "./masterQueue.js"; @@ -63,7 +64,7 @@ export * from "./workerQueue.js"; * * @typeParam TPayloadSchema - Zod schema for message payload validation */ -export class FairQueue { +export class FairQueue { private redis: Redis; private keys: FairQueueKeyProducer; private scheduler: FairScheduler; @@ -85,7 +86,9 @@ export class FairQueue { private visibilityTimeoutMs: number; private heartbeatIntervalMs: number; private reclaimIntervalMs: number; - private workerQueueResolver: (message: StoredMessage>) => string; + private workerQueueResolver: ( + message: StoredMessage> + ) => string; private batchClaimSize: number; // Cooloff state @@ -264,7 +267,7 @@ export class FairQueue { /** * Enqueue a single message to a queue. */ - async enqueue(options: EnqueueOptions>): Promise { + async enqueue(options: EnqueueOptions>): Promise { return this.telemetry.trace( "enqueue", async (span) => { @@ -293,7 +296,7 @@ export class FairQueue { this.queueDescriptorCache.set(options.queueId, descriptor); // Build stored message - const storedMessage: StoredMessage> = { + const storedMessage: StoredMessage> = { id: messageId, queueId: options.queueId, tenantId: options.tenantId, @@ -356,7 +359,9 @@ export class FairQueue { /** * Enqueue multiple messages to a queue. */ - async enqueueBatch(options: EnqueueBatchOptions>): Promise { + async enqueueBatch( + options: EnqueueBatchOptions> + ): Promise { return this.telemetry.trace( "enqueueBatch", async (span) => { @@ -392,7 +397,7 @@ export class FairQueue { } } - const storedMessage: StoredMessage> = { + const storedMessage: StoredMessage> = { id: messageId, queueId: options.queueId, tenantId: options.tenantId, @@ -463,7 +468,7 @@ export class FairQueue { async getDeadLetterMessages( tenantId: string, limit: number = 100 - ): Promise>[]> { + ): Promise>[]> { if (!this.deadLetterQueueEnabled) { return []; } @@ -474,7 +479,7 @@ export class FairQueue { // Get message IDs with scores (deadLetteredAt timestamps) const results = await this.redis.zrange(dlqKey, 0, limit - 1, "WITHSCORES"); - const messages: DeadLetterMessage>[] = []; + const messages: DeadLetterMessage>[] = []; for (let i = 0; i < results.length; i += 2) { const messageId = results[i]; @@ -485,7 +490,9 @@ export class FairQueue { if (!dataJson) continue; try { - const data = JSON.parse(dataJson) as DeadLetterMessage>; + const data = JSON.parse(dataJson) as DeadLetterMessage< + inferZodSchemaOutput + >; data.deadLetteredAt = parseFloat(deadLetteredAtStr); messages.push(data); } catch { @@ -516,7 +523,9 @@ export class FairQueue { return false; } - const dlqMessage = JSON.parse(dataJson) as DeadLetterMessage>; + const dlqMessage = JSON.parse(dataJson) as DeadLetterMessage< + inferZodSchemaOutput + >; // Re-enqueue with reset attempt count await this.enqueue({ @@ -1124,7 +1133,7 @@ export class FairQueue { // Claim batch of messages with visibility timeout const claimedMessages = await this.visibilityManager.claimBatch< - StoredMessage> + StoredMessage> >(queueId, queueKey, queueItemsKey, loopId, maxClaimCount, this.visibilityTimeoutMs); if (claimedMessages.length === 0) { @@ -1194,7 +1203,7 @@ export class FairQueue { async getMessageData( messageId: string, queueId: string - ): Promise> | null> { + ): Promise> | null> { const shardId = this.masterQueue.getShardForQueue(queueId); const inflightDataKey = this.keys.inflightDataKey(shardId); const dataJson = await this.redis.hget(inflightDataKey, messageId); @@ -1204,7 +1213,7 @@ export class FairQueue { } try { - return JSON.parse(dataJson) as StoredMessage>; + return JSON.parse(dataJson) as StoredMessage>; } catch { this.logger.error("Failed to parse message data", { messageId, queueId }); return null; @@ -1236,7 +1245,7 @@ export class FairQueue { // Get stored message for concurrency release const dataJson = await this.redis.hget(inflightDataKey, messageId); - let storedMessage: StoredMessage> | null = null; + let storedMessage: StoredMessage> | null = null; if (dataJson) { try { storedMessage = JSON.parse(dataJson); @@ -1291,7 +1300,7 @@ export class FairQueue { // Get stored message for concurrency release const dataJson = await this.redis.hget(inflightDataKey, messageId); - let storedMessage: StoredMessage> | null = null; + let storedMessage: StoredMessage> | null = null; if (dataJson) { try { storedMessage = JSON.parse(dataJson); @@ -1356,7 +1365,7 @@ export class FairQueue { return; } - let storedMessage: StoredMessage>; + let storedMessage: StoredMessage>; try { storedMessage = JSON.parse(dataJson); } catch { @@ -1390,7 +1399,7 @@ export class FairQueue { // ============================================================================ async #handleMessageFailure( - storedMessage: StoredMessage>, + storedMessage: StoredMessage>, queueId: string, queueKey: string, queueItemsKey: string, @@ -1455,7 +1464,7 @@ export class FairQueue { } async #moveToDeadLetterQueue( - storedMessage: StoredMessage>, + storedMessage: StoredMessage>, errorMessage?: string ): Promise { if (!this.deadLetterQueueEnabled) { @@ -1468,7 +1477,7 @@ export class FairQueue { const dlqDataKey = this.keys.deadLetterQueueDataKey(storedMessage.tenantId); const _shardId = this.masterQueue.getShardForQueue(storedMessage.queueId); - const dlqMessage: DeadLetterMessage> = { + const dlqMessage: DeadLetterMessage> = { id: storedMessage.id, queueId: storedMessage.queueId, tenantId: storedMessage.tenantId, diff --git a/packages/redis-worker/src/fair-queue/tests/fairQueue.test.ts b/packages/redis-worker/src/fair-queue/tests/fairQueue.test.ts index 5ae1f390f9f..9e7c8c6834d 100644 --- a/packages/redis-worker/src/fair-queue/tests/fairQueue.test.ts +++ b/packages/redis-worker/src/fair-queue/tests/fairQueue.test.ts @@ -1,6 +1,6 @@ import { describe, expect, vi } from "vitest"; import { redisTest } from "@internal/testcontainers"; -import { z } from "zod"; +import { z } from "zod/v4"; import { FairQueue, DefaultFairQueueKeyProducer, diff --git a/packages/redis-worker/src/fair-queue/tests/raceConditions.test.ts b/packages/redis-worker/src/fair-queue/tests/raceConditions.test.ts index a497e2d738d..c6c8e2d5a6a 100644 --- a/packages/redis-worker/src/fair-queue/tests/raceConditions.test.ts +++ b/packages/redis-worker/src/fair-queue/tests/raceConditions.test.ts @@ -1,6 +1,6 @@ import { describe, expect, vi } from "vitest"; import { redisTest } from "@internal/testcontainers"; -import { z } from "zod"; +import { z } from "zod/v4"; import { FairQueue, DefaultFairQueueKeyProducer, diff --git a/packages/redis-worker/src/fair-queue/tests/tenantDispatch.test.ts b/packages/redis-worker/src/fair-queue/tests/tenantDispatch.test.ts index 5f02ac331f5..e03e0b31ad6 100644 --- a/packages/redis-worker/src/fair-queue/tests/tenantDispatch.test.ts +++ b/packages/redis-worker/src/fair-queue/tests/tenantDispatch.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "vitest"; import { redisTest } from "@internal/testcontainers"; import { createRedisClient } from "@internal/redis"; -import { z } from "zod"; +import { z } from "zod/v4"; import { FairQueue, DefaultFairQueueKeyProducer, diff --git a/packages/redis-worker/src/fair-queue/types.ts b/packages/redis-worker/src/fair-queue/types.ts index 3bc56b599fa..098c1116080 100644 --- a/packages/redis-worker/src/fair-queue/types.ts +++ b/packages/redis-worker/src/fair-queue/types.ts @@ -1,7 +1,8 @@ import type { RedisOptions } from "@internal/redis"; import type { Logger } from "@trigger.dev/core/logger"; +import type { AnyZodSchema, inferZodSchemaOutput } from "@trigger.dev/core/v3"; import type { Tracer, Meter } from "@internal/tracing"; -import type { z } from "zod"; +import type { z } from "zod/v4"; import type { RetryStrategy } from "./retry.js"; // ============================================================================ @@ -371,7 +372,7 @@ export interface CooloffOptions { * * @typeParam TPayloadSchema - Zod schema for message payload validation */ -export interface FairQueueOptions { +export interface FairQueueOptions { /** Redis connection options */ redis: RedisOptions; @@ -400,7 +401,7 @@ export interface FairQueueOptions>; + workerQueue: WorkerQueueOptions>; // Retry and DLQ /** Retry and dead letter queue configuration */ diff --git a/packages/redis-worker/src/mollifier/schemas.ts b/packages/redis-worker/src/mollifier/schemas.ts index db75ed6e452..4d3b77a7b11 100644 --- a/packages/redis-worker/src/mollifier/schemas.ts +++ b/packages/redis-worker/src/mollifier/schemas.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; export const BufferEntryStatus = z.enum(["QUEUED", "DRAINING", "FAILED"]); export type BufferEntryStatus = z.infer; @@ -56,12 +56,12 @@ export const BufferEntrySchema = z.object({ // so an entry written by an accept Lua predating this field — or one // surviving across the deploy that introduced it — still parses instead // of being silently dropped on pop. - createdAtMicros: stringToInt.default("0"), + createdAtMicros: stringToInt.default(0), // Drainer-ack flag: `true` once the drainer has materialised this run // into PG. The hash persists for a short grace TTL after ack so direct // reads (retrieve, trace, etc.) still resolve while PG replica lag // settles. Absent on pre-ack entries. - materialised: stringToBool.default("false"), + materialised: stringToBool.default(false), // Denormalised pointer to the Redis idempotency lookup key (set when // the run was accepted with an idempotency key, empty otherwise). The // ack Lua reads this to DEL the lookup atomically with marking the @@ -71,7 +71,7 @@ export const BufferEntrySchema = z.object({ // Incremented atomically by the CAS metadata Lua. Matches the // semantic of `TaskRun.metadataVersion` on the PG side (which the // UpdateMetadataService uses for the same retry-on-conflict pattern). - metadataVersion: stringToInt.default("0"), + metadataVersion: stringToInt.default(0), lastError: stringToError.optional(), }); diff --git a/packages/redis-worker/src/queue.test.ts b/packages/redis-worker/src/queue.test.ts index 8d72c2ca0b9..26b0e1ccab9 100644 --- a/packages/redis-worker/src/queue.test.ts +++ b/packages/redis-worker/src/queue.test.ts @@ -1,7 +1,7 @@ import { redisTest } from "@internal/testcontainers"; import { describe } from "node:test"; import { expect } from "vitest"; -import { z } from "zod"; +import { z } from "zod/v4"; import { SimpleQueue } from "./queue.js"; import { Logger } from "@trigger.dev/core/logger"; import { createRedisClient } from "@internal/redis"; diff --git a/packages/redis-worker/src/queue.ts b/packages/redis-worker/src/queue.ts index becfb3813ea..0918b827f63 100644 --- a/packages/redis-worker/src/queue.ts +++ b/packages/redis-worker/src/queue.ts @@ -6,18 +6,18 @@ import { type Result, } from "@internal/redis"; import { Logger } from "@trigger.dev/core/logger"; +import type { AnyZodSchema, inferZodSchemaOutput } from "@trigger.dev/core/v3"; import { nanoid } from "nanoid"; -import type { z } from "zod"; export interface MessageCatalogSchema { - [key: string]: z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion; + [key: string]: AnyZodSchema; } export type MessageCatalogKey = keyof TMessageCatalog; export type MessageCatalogValue< TMessageCatalog extends MessageCatalogSchema, TKey extends MessageCatalogKey, -> = z.infer; +> = inferZodSchemaOutput; export type AnyMessageCatalog = MessageCatalogSchema; export type QueueItem = { @@ -217,6 +217,7 @@ export class SimpleQueue { continue; } + // zod v4 narrows the generic schema union here; cast to public ZodType (has safeParse in v3 & v4) const validatedItem = schema.safeParse(parsedItem.item); if (!validatedItem.success) { @@ -236,7 +237,7 @@ export class SimpleQueue { dequeuedItems.push({ id, job: parsedItem.job, - item: validatedItem.data, + item: validatedItem.data as QueueItem["item"], visibilityTimeoutMs, attempt: parsedItem.attempt ?? 0, timestamp, diff --git a/packages/redis-worker/src/worker.test.ts b/packages/redis-worker/src/worker.test.ts index f5659f3795b..10f373b3c79 100644 --- a/packages/redis-worker/src/worker.test.ts +++ b/packages/redis-worker/src/worker.test.ts @@ -2,7 +2,7 @@ import { isolatedRedisTest as redisTest } from "@internal/testcontainers"; import { Logger } from "@trigger.dev/core/logger"; import { describe } from "node:test"; import { expect } from "vitest"; -import { z } from "zod"; +import { z } from "zod/v4"; import { Worker } from "./worker.js"; import { createRedisClient } from "@internal/redis"; diff --git a/packages/redis-worker/src/worker.ts b/packages/redis-worker/src/worker.ts index b9240acaa63..dc4fe092f26 100644 --- a/packages/redis-worker/src/worker.ts +++ b/packages/redis-worker/src/worker.ts @@ -12,12 +12,16 @@ import { ValueType, } from "@internal/tracing"; import { Logger } from "@trigger.dev/core/logger"; -import { calculateNextRetryDelay } from "@trigger.dev/core/v3"; +import { + calculateNextRetryDelay, + type AnyZodSchema, + type inferZodSchemaOutput, +} from "@trigger.dev/core/v3"; import { type RetryOptions } from "@trigger.dev/core/v3/schemas"; import { shutdownManager } from "@trigger.dev/core/v3/serverOnly"; import { nanoid } from "nanoid"; import pLimit from "p-limit"; -import { z } from "zod"; +import { z } from "zod/v4"; import { type AnyQueueItem, SimpleQueue } from "./queue.js"; import { parseExpression } from "cron-parser"; @@ -36,7 +40,7 @@ export type BatchConfig = { export type WorkerCatalog = { [key: string]: { - schema: z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion; + schema: AnyZodSchema; visibilityTimeoutMs: number; retry?: RetryOptions; cron?: string; @@ -56,7 +60,7 @@ type QueueCatalogFromWorkerCatalog = { export type JobHandlerParams = { id: string; - payload: z.infer; + payload: inferZodSchemaOutput; visibilityTimeoutMs: number; attempt: number; deduplicationKey?: string; @@ -311,7 +315,7 @@ class Worker { }: { id?: string; job: K; - payload: z.infer; + payload: inferZodSchemaOutput; visibilityTimeoutMs?: number; availableAt?: Date; cancellationKey?: string; @@ -373,7 +377,7 @@ class Worker { }: { id: string; job: K; - payload: z.infer; + payload: inferZodSchemaOutput; visibilityTimeoutMs?: number; availableAt?: Date; }) { @@ -1088,11 +1092,13 @@ class Worker { const enqueued = await this.enqueueOnce({ id: identifier, job, + // job is a runtime string here, so K widens and the payload can't match a + // single catalog entry statically; the shape is correct for the cron job. payload: { timestamp: scheduledAt.getTime(), lastTimestamp: lastTimestamp?.getTime(), cron, - }, + } as inferZodSchemaOutput, availableAt, }); diff --git a/packages/schema-to-json/package.json b/packages/schema-to-json/package.json index 0a590345abc..795dfc51651 100644 --- a/packages/schema-to-json/package.json +++ b/packages/schema-to-json/package.json @@ -42,9 +42,9 @@ }, "dependencies": { "@trigger.dev/core": "workspace:*", - "zod-to-json-schema": "^3.24.0", + "zod-to-json-schema": "^3.25.0", "@sodaru/yup-to-json-schema": "^2", - "zod": "3.25.76", + "zod": "^3.25.76 || ^4.0.0", "effect": "^3" }, "devDependencies": { @@ -59,6 +59,7 @@ "@arethetypeswrong/cli": "^0.18.5" }, "peerDependencies": { + "zod": "^3.25.76 || ^4.0.0", "arktype": ">=2.0.0", "runtypes": ">=5.0.0", "superstruct": ">=0.14.2", diff --git a/packages/schema-to-json/src/index.ts b/packages/schema-to-json/src/index.ts index a02eef07aa3..ebaf45c7348 100644 --- a/packages/schema-to-json/src/index.ts +++ b/packages/schema-to-json/src/index.ts @@ -1,7 +1,7 @@ // 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 * as z4 from "zod/v4/core"; import { convertSchema } from "@sodaru/yup-to-json-schema"; import { JSONSchema as EffectJSONSchema } from "effect"; @@ -41,28 +41,32 @@ export function schemaToJsonSchema( schema: Schema, options?: ConversionOptions ): ConversionResult | undefined { + if (schema === null || (typeof schema !== "object" && typeof schema !== "function")) { + return undefined; + } + const parser = schema as any; - // Check if schema has a built-in toJsonSchema method (e.g., ArkType, Zod 4) - if (typeof parser.toJsonSchema === "function") { - try { - const jsonSchema = parser.toJsonSchema(); + if (isZodSchema(parser)) { + const jsonSchema = convertZodSchema(parser, options); + if (jsonSchema) { return { - jsonSchema, + jsonSchema: jsonSchema, }; - } catch (_error) { - // If toJsonSchema fails, continue to other checks } } - if (isZodSchema(parser)) { - const jsonSchema = convertZodSchema(parser, options); + // Check if schema has a built-in toJsonSchema method (e.g., ArkType) + if (typeof parser.toJsonSchema === "function") { + try { + const jsonSchema = parser.toJsonSchema(); - if (jsonSchema) { return { - jsonSchema: jsonSchema, + jsonSchema, }; + } catch (_error) { + // If toJsonSchema fails, continue to other checks } } @@ -111,11 +115,18 @@ export function isZodSchema(schema: any): boolean { } function isZod3Schema(schema: any): boolean { - return "_def" in schema && "parse" in schema && "parseAsync" in schema && "safeParse" in schema; + return ( + typeof schema === "object" && + schema !== null && + "_def" in schema && + "parse" in schema && + "parseAsync" in schema && + "safeParse" in schema + ); } function isZod4Schema(schema: any): boolean { - return "_zod" in schema; + return typeof schema === "object" && schema !== null && "_zod" in schema; } function convertZodSchema(schema: any, options?: ConversionOptions): JSONSchema | undefined { diff --git a/packages/schema-to-json/tests/index.test.ts b/packages/schema-to-json/tests/index.test.ts index b266a42b3b0..a886085588f 100644 --- a/packages/schema-to-json/tests/index.test.ts +++ b/packages/schema-to-json/tests/index.test.ts @@ -265,6 +265,7 @@ describe("canConvertSchema", () => { }); it("should return false for unsupported schemas", () => { + expect(canConvertSchema(null)).toBe(false); expect(canConvertSchema({ notASchema: true })).toBe(false); expect(canConvertSchema(() => true)).toBe(false); }); diff --git a/packages/trigger-sdk/package.json b/packages/trigger-sdk/package.json index 64de5ea6d96..07b5ae8b6d5 100644 --- a/packages/trigger-sdk/package.json +++ b/packages/trigger-sdk/package.json @@ -101,13 +101,13 @@ "tshy": "^3.0.2", "tsx": "4.17.0", "typed-emitter": "^2.1.0", - "zod": "3.25.76" + "zod": "4.4.3" }, "peerDependencies": { "@ai-sdk/otel": ">=1.0.0-0 <2", "ai": "^5.0.0 || ^6.0.0 || >=7.0.0-canary <8", "react": "^18.0 || ^19.0", - "zod": "^3.0.0 || ^4.0.0" + "zod": "^3.25.76 || ^4.0.0" }, "peerDependenciesMeta": { "@ai-sdk/otel": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45163c161bb..71ed63149f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,7 @@ overrides: brace-expansion@>=5 <5.0.6: 5.0.6 '@jsonhero/json-infer-types>ip-address': ^10.2.0 '@modelcontextprotocol/sdk@>=1.26.0>express-rate-limit': ^8.6.0 + zod: 4.4.3 patchedDependencies: '@changesets/assemble-release-plan@5.2.4': @@ -191,8 +192,8 @@ importers: specifier: ^3.8.0 version: 3.8.1 zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.4.3 + version: 4.4.3 devDependencies: '@internal/testcontainers': specifier: workspace:* @@ -205,13 +206,13 @@ importers: dependencies: '@ai-sdk/anthropic': specifier: ^3.0.0 - version: 3.0.84(zod@3.25.76) + version: 3.0.84(zod@4.4.3) '@ai-sdk/openai': specifier: ^3.0.0 - version: 3.0.41(zod@3.25.76) + version: 3.0.41(zod@4.4.3) '@ai-sdk/react': specifier: ^3.0.0 - version: 3.0.170(react@18.3.1)(zod@3.25.76) + version: 3.0.170(react@18.3.1)(zod@4.4.3) '@ariakit/react': specifier: ^0.4.6 version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -268,7 +269,7 @@ importers: version: 1.19.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@conform-to/zod': specifier: ^1.2.2 - version: 1.19.4(zod@3.25.76) + version: 1.19.4(zod@4.4.3) '@depot/cli': specifier: 0.0.1-cli.2.80.0 version: 0.0.1-cli.2.80.0 @@ -489,8 +490,8 @@ importers: specifier: workspace:* version: link:../../internal-packages/otlp-importer '@trigger.dev/platform': - specifier: 1.3.0 - version: 1.3.0(zod@3.25.76) + specifier: 1.3.1 + version: 1.3.1(zod@4.4.3) '@trigger.dev/plugins': specifier: workspace:* version: link:../../packages/plugins @@ -526,7 +527,7 @@ importers: version: 1.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ai: specifier: 6.0.116 - version: 6.0.116(zod@3.25.76) + version: 6.0.116(zod@4.4.3) assert: specifier: ^2.1.0 version: 2.1.0 @@ -727,7 +728,7 @@ importers: version: 0.3.1(@remix-run/react@2.17.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@remix-run/server-runtime@2.17.5(typescript@6.0.3))(react@18.3.1) remix-utils: specifier: ^7.7.0 - version: 7.7.0(@remix-run/node@2.17.5(typescript@6.0.3))(@remix-run/react@2.17.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@remix-run/router@1.23.3(patch_hash=33e2966e9ef36aa09955cec9922c5063227b83429157a71ba226369a08627ac9))(crypto-js@4.2.0)(intl-parse-accept-language@1.0.0)(react@18.3.1)(zod@3.25.76) + version: 7.7.0(@remix-run/node@2.17.5(typescript@6.0.3))(@remix-run/react@2.17.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@remix-run/router@1.23.3(patch_hash=33e2966e9ef36aa09955cec9922c5063227b83429157a71ba226369a08627ac9))(crypto-js@4.2.0)(intl-parse-accept-language@1.0.0)(react@18.3.1)(zod@4.4.3) semver: specifier: ^7.5.0 version: 7.8.1 @@ -777,14 +778,14 @@ importers: specifier: 8.21.0 version: 8.21.0(bufferutil@4.0.9) zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.4.3 + version: 4.4.3 zod-error: - specifier: 1.5.0 - version: 1.5.0 + specifier: 2.0.0 + version: 2.0.0(zod@4.4.3) zod-validation-error: - specifier: ^1.5.0 - version: 1.5.0(zod@3.25.76) + specifier: ^5.0.0 + version: 5.0.0(zod@4.4.3) devDependencies: '@internal/clickhouse': specifier: workspace:* @@ -899,7 +900,7 @@ importers: version: 0.15.18 evalite: specifier: 1.0.0-beta.16 - version: 1.0.0-beta.16(ai@6.0.116(zod@3.25.76))(better-sqlite3@11.10.0)(bufferutil@4.0.9) + version: 1.0.0-beta.16(ai@6.0.116(zod@4.4.3))(better-sqlite3@11.10.0)(bufferutil@4.0.9) npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -975,11 +976,11 @@ importers: specifier: workspace:* version: link:../../packages/core zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.4.3 + version: 4.4.3 zod-error: - specifier: 1.5.0 - version: 1.5.0 + specifier: 2.0.0 + version: 2.0.0(zod@4.4.3) devDependencies: '@internal/testcontainers': specifier: workspace:* @@ -991,8 +992,8 @@ importers: internal-packages/compute: dependencies: zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.4.3 + version: 4.4.3 devDependencies: rimraf: specifier: 6.0.1 @@ -1002,7 +1003,7 @@ importers: dependencies: '@ai-sdk/anthropic': specifier: ^3.0.0 - version: 3.0.84(zod@3.25.76) + version: 3.0.84(zod@4.4.3) '@internal/dashboard-agent-db': specifier: workspace:* version: link:../dashboard-agent-db @@ -1011,10 +1012,10 @@ importers: version: link:../../packages/trigger-sdk ai: specifier: 6.0.116 - version: 6.0.116(zod@3.25.76) + version: 6.0.116(zod@4.4.3) zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.4.3 + version: 4.4.3 devDependencies: '@ai-sdk/provider': specifier: 3.0.8 @@ -1088,8 +1089,8 @@ importers: specifier: ^1.2.0 version: 1.3.1 zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.4.3 + version: 4.4.3 devDependencies: '@types/nodemailer': specifier: ^8.0.0 @@ -1256,8 +1257,8 @@ importers: specifier: ^3.0.5 version: 3.0.5 zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.4.3 + version: 4.4.3 devDependencies: '@internal/testcontainers': specifier: workspace:* @@ -1334,8 +1335,8 @@ importers: specifier: 3.3.8 version: 3.3.8 zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.4.3 + version: 4.4.3 devDependencies: '@internal/testcontainers': specifier: workspace:* @@ -1446,8 +1447,8 @@ importers: specifier: 0.5.0-alpha.4 version: 0.5.0-alpha.4(patch_hash=b5d41129ddbf7c4cbb0288244b2c8d041ee0803c5102c1181c180408d3b579f4) zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.4.3 + version: 4.4.3 devDependencies: antlr4ts-cli: specifier: 0.5.0-alpha.4 @@ -1506,7 +1507,7 @@ importers: version: 0.0.1-cli.2.80.0 '@modelcontextprotocol/sdk': specifier: ^1.25.2 - version: 1.25.2(hono@4.12.25)(supports-color@10.0.0)(zod@3.25.76) + version: 1.25.2(hono@4.12.25)(supports-color@10.0.0)(zod@4.4.3) '@opentelemetry/api': specifier: 1.9.1 version: 1.9.1 @@ -1685,11 +1686,11 @@ importers: specifier: ^8.3.0 version: 8.3.0 zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.4.3 + version: 4.4.3 zod-validation-error: - specifier: ^1.5.0 - version: 1.5.0(zod@3.25.76) + specifier: ^5.0.0 + version: 5.0.0(zod@4.4.3) devDependencies: '@epic-web/test-server': specifier: ^0.1.0 @@ -1845,18 +1846,15 @@ importers: specifier: ^0.1.3 version: 0.1.3 zod: - specifier: 3.25.76 - version: 3.25.76 - zod-error: - specifier: 1.5.0 - version: 1.5.0 + specifier: 4.4.3 + version: 4.4.3 zod-validation-error: - specifier: ^1.5.0 - version: 1.5.0(zod@3.25.76) + specifier: ^5.0.0 + version: 5.0.0(zod@4.4.3) devDependencies: '@ai-sdk/provider-utils': specifier: ^1.0.22 - version: 1.0.22(zod@3.25.76) + version: 1.0.22(zod@4.4.3) '@arethetypeswrong/cli': specifier: ^0.18.5 version: 0.18.5 @@ -1877,10 +1875,10 @@ importers: version: 4.0.14 ai: specifier: 6.0.116 - version: 6.0.116(zod@3.25.76) + version: 6.0.116(zod@4.4.3) ai-v7: specifier: npm:ai@7.0.0-canary.159 - version: ai@7.0.0-canary.159(zod@3.25.76) + version: ai@7.0.0-canary.159(zod@4.4.3) defu: specifier: ^6.1.5 version: 6.1.7 @@ -2017,8 +2015,8 @@ importers: specifier: ^3.0.5 version: 3.0.5 zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.4.3 + version: 4.4.3 devDependencies: '@internal/redis': specifier: workspace:* @@ -2097,11 +2095,11 @@ importers: specifier: ^3 version: 3.17.1 zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.4.3 + version: 4.4.3 zod-to-json-schema: - specifier: ^3.24.0 - version: 3.24.6(zod@3.25.76) + specifier: ^3.25.0 + version: 3.25.1(zod@4.4.3) devDependencies: '@arethetypeswrong/cli': specifier: ^0.18.5 @@ -2135,7 +2133,7 @@ importers: dependencies: '@ai-sdk/otel': specifier: '>=1.0.0-0 <2' - version: 1.0.0-beta.6(zod@3.25.76) + version: 1.0.0-beta.6(zod@4.4.3) '@opentelemetry/api': specifier: 1.9.1 version: 1.9.1 @@ -2193,10 +2191,10 @@ importers: version: 8.5.4 ai: specifier: 6.0.116 - version: 6.0.116(zod@3.25.76) + version: 6.0.116(zod@4.4.3) ai-v7: specifier: npm:ai@7.0.0-canary.159 - version: ai@7.0.0-canary.159(zod@3.25.76) + version: ai@7.0.0-canary.159(zod@4.4.3) encoding: specifier: ^0.1.13 version: 0.1.13 @@ -2213,8 +2211,8 @@ importers: specifier: ^2.1.0 version: 2.1.0 zod: - specifier: 3.25.76 - version: 3.25.76 + specifier: 4.4.3 + version: 4.4.3 packages: @@ -2222,31 +2220,31 @@ packages: resolution: {integrity: sha512-BIDaHmCHs6Sr5VUsEkTbbVlAN4GWjg97X9x/IfXyviLtzsXvffui9XIcZugkAi1Ri6FnvI5T5qDGh5YLnSuzRg==} engines: {node: '>=18'} peerDependencies: - zod: ^3.25.76 || ^4.1.8 + zod: 4.4.3 '@ai-sdk/gateway@3.0.66': resolution: {integrity: sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A==} engines: {node: '>=18'} peerDependencies: - zod: ^3.25.76 || ^4.1.8 + zod: 4.4.3 '@ai-sdk/gateway@4.0.0-beta.30': resolution: {integrity: sha512-05tWxCb+3SRj5LBY37nRQ8eUdolO5QvKAfsIAUY3zAxg0jc1cBkQNSpZEVwQUR4uFm/Y5fLJ+MIa+6hkol1//A==} engines: {node: '>=18'} peerDependencies: - zod: ^3.25.76 || ^4.1.8 + zod: 4.4.3 '@ai-sdk/gateway@4.0.0-canary.93': resolution: {integrity: sha512-/d0d/DfJRmdMJJww/ihrYezgKedJhrOCsREuAuSHmd/kVzFB5p2bBuCZH3EBxKFQ8NLtPJpOR+QJxOUmDLD8PA==} engines: {node: '>=22'} peerDependencies: - zod: ^3.25.76 || ^4.1.8 + zod: 4.4.3 '@ai-sdk/openai@3.0.41': resolution: {integrity: sha512-IZ42A+FO+vuEQCVNqlnAPYQnnUpUfdJIwn1BEDOBywiEHa23fw7PahxVtlX9zm3/zMvTW4JKPzWyvAgDu+SQ2A==} engines: {node: '>=18'} peerDependencies: - zod: ^3.25.76 || ^4.1.8 + zod: 4.4.3 '@ai-sdk/otel@1.0.0-beta.6': resolution: {integrity: sha512-K5VikyO3EKQkNk77ew9oMjM8FInKF+WWar599LmP8rQ0x0iB+P/DVS+h6zQvmecxMNPtQOOyt0uDQFx/AA0DGw==} @@ -2256,7 +2254,7 @@ packages: resolution: {integrity: sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==} engines: {node: '>=18'} peerDependencies: - zod: ^3.0.0 + zod: 4.4.3 peerDependenciesMeta: zod: optional: true @@ -2265,19 +2263,19 @@ packages: resolution: {integrity: sha512-uhukHaCBvqkwBHkT8C2PrnqKTCoLn3pdHXqtcR9I8ErH+flbzgW4o7VHSNIup9LRu+WBvZIZDQLsx6rwl2tiOA==} engines: {node: '>=18'} peerDependencies: - zod: ^3.25.76 || ^4.1.8 + zod: 4.4.3 '@ai-sdk/provider-utils@5.0.0-beta.9': resolution: {integrity: sha512-Ku/S1TV8ibqlweKQHa22eWT9QvKqR4hkl7KMJ0k7kw64Q7XObh12fJUnK4Mh8q+IR5ZeQGm95SLtx++d7FC6XA==} engines: {node: '>=18'} peerDependencies: - zod: ^3.25.76 || ^4.1.8 + zod: 4.4.3 '@ai-sdk/provider-utils@5.0.0-canary.44': resolution: {integrity: sha512-3h7gp8MEwXACWsW8ZPjzio53ynKq0jTNp4Hwr0BWdychMFzmkRRWyvijhg56JWxye16FU92SqQnfBoe2PN4I2A==} engines: {node: '>=22'} peerDependencies: - zod: ^3.25.76 || ^4.1.8 + zod: 4.4.3 '@ai-sdk/provider@0.0.26': resolution: {integrity: sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==} @@ -3149,7 +3147,7 @@ packages: '@conform-to/zod@1.19.4': resolution: {integrity: sha512-g80QtotXLcXWCF86Xou/VpI6i+NBOQxYgLS9YCQYrgV27yilZFJYpgWWBsA8YwodmJVBIYm9RawJba7WnUoddA==} peerDependencies: - zod: ^3.21.0 || ^4.0.0 + zod: 4.4.3 '@connectrpc/connect-node@1.4.0': resolution: {integrity: sha512-0ANnrr6SvsjevsWEgdzHy7BaHkluZyS6s4xNoVt7RBHFR5V/kT9lPokoIbYUOU9JHzdRgTaS3x5595mwUsu15g==} @@ -4607,7 +4605,7 @@ packages: engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 + zod: 4.4.3 peerDependenciesMeta: '@cfworker/json-schema': optional: true @@ -4617,7 +4615,7 @@ packages: engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 + zod: 4.4.3 peerDependenciesMeta: '@cfworker/json-schema': optional: true @@ -7727,10 +7725,10 @@ packages: react: 18.3.1 react-dom: 18.3.1 - '@trigger.dev/platform@1.3.0': - resolution: {integrity: sha512-DIJ5Sy4X5XKSHQdYA3GImkb0jdE9I/HTRRRB3c9CFuXjGzhK3rJkEzjIEbaNRjk6yAev67EVTWY8P+D+khorUw==} + '@trigger.dev/platform@1.3.1': + resolution: {integrity: sha512-eCmoeilJyat6APofjHnyiJzOYobHbukbmexDpZVbQlqAkJaXKsK/f07Ijuw8Cyx3kTj8Hc24fj2vMHFPwcE9VA==} peerDependencies: - zod: ^3.25.0 || ^4.0.0 + zod: 4.4.3 '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -8406,19 +8404,19 @@ packages: resolution: {integrity: sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA==} engines: {node: '>=18'} peerDependencies: - zod: ^3.25.76 || ^4.1.8 + zod: 4.4.3 ai@7.0.0-beta.60: resolution: {integrity: sha512-grSKE/9kQtElLCnrhwriTLwXlV+5W8Ljco2KzVc5cY9egK1zwSwVyFQo3L3Al+WVxwvVT6fQjLyEdQpqJHLTrA==} engines: {node: '>=18'} peerDependencies: - zod: ^3.25.76 || ^4.1.8 + zod: 4.4.3 ai@7.0.0-canary.159: resolution: {integrity: sha512-9z8OkZdrISDjS3bguQ/mCYsthmr7mNbB1hTa8cRg8aAp31r1HCxVFqmc+ZXTA0FqhGb8aW46UUcjUuJcwxg5Mw==} engines: {node: '>=22'} peerDependencies: - zod: ^3.25.76 || ^4.1.8 + zod: 4.4.3 ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} @@ -12613,7 +12611,7 @@ packages: hasBin: true peerDependencies: ws: 8.21.0 - zod: ^3.23.8 + zod: 4.4.3 peerDependenciesMeta: ws: optional: true @@ -13769,7 +13767,7 @@ packages: intl-parse-accept-language: ^1.0.0 is-ip: ^5.0.1 react: 18.3.1 - zod: ^3.22.4 + zod: 4.4.3 peerDependenciesMeta: '@remix-run/cloudflare': optional: true @@ -15597,37 +15595,31 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} - zod-error@1.5.0: - resolution: {integrity: sha512-zzopKZ/skI9iXpqCEPj+iLCKl9b88E43ehcU+sbRoHuwGd9F1IDVGQ70TyO6kmfiRL1g4IXkjsXK+g1gLYl4WQ==} + zod-error@2.0.0: + resolution: {integrity: sha512-ivSoPm7h5SEGG8GUvQ7YTwrnEx36qNmUIU5hSIGcqcdl7+ZFoTrYXAdQEs3KP/rkihrrR9REEdOeGU+fWdMoNw==} + peerDependencies: + zod: 4.4.3 zod-to-json-schema@3.24.5: resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} peerDependencies: - zod: ^3.24.1 - - zod-to-json-schema@3.24.6: - resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} - peerDependencies: - zod: ^3.24.1 + zod: 4.4.3 zod-to-json-schema@3.25.0: resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==} peerDependencies: - zod: ^3.25 || ^4 + zod: 4.4.3 zod-to-json-schema@3.25.1: resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: - zod: ^3.25 || ^4 + zod: 4.4.3 - zod-validation-error@1.5.0: - resolution: {integrity: sha512-/7eFkAI4qV0tcxMBB/3+d2c1P6jzzZYdYSlBuAklzMuCrJu5bzJfHS0yVAS87dRHVlhftd6RFJDIvv03JgkSbw==} - engines: {node: '>=16.0.0'} + zod-validation-error@5.0.0: + resolution: {integrity: sha512-hmk+pkyKq7Q71PiWVSDUc3VfpzpvcRHZ3QPw9yEMVvmtCekaMeOHnbr3WbxfrgEnQTv6haGP4cmv0Ojmihzsxw==} + engines: {node: '>=18.0.0'} peerDependencies: - zod: ^3.18.0 - - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod: 4.4.3 zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -15637,77 +15629,77 @@ packages: snapshots: - '@ai-sdk/anthropic@3.0.84(zod@3.25.76)': + '@ai-sdk/anthropic@3.0.84(zod@4.4.3)': dependencies: '@ai-sdk/provider': 3.0.10 - '@ai-sdk/provider-utils': 4.0.29(zod@3.25.76) - zod: 3.25.76 + '@ai-sdk/provider-utils': 4.0.29(zod@4.4.3) + zod: 4.4.3 - '@ai-sdk/gateway@3.0.66(zod@3.25.76)': + '@ai-sdk/gateway@3.0.66(zod@4.4.3)': dependencies: '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.29(zod@3.25.76) + '@ai-sdk/provider-utils': 4.0.29(zod@4.4.3) '@vercel/oidc': 3.1.0 - zod: 3.25.76 + zod: 4.4.3 - '@ai-sdk/gateway@4.0.0-beta.30(zod@3.25.76)': + '@ai-sdk/gateway@4.0.0-beta.30(zod@4.4.3)': dependencies: '@ai-sdk/provider': 4.0.0-beta.5 - '@ai-sdk/provider-utils': 5.0.0-beta.9(zod@3.25.76) + '@ai-sdk/provider-utils': 5.0.0-beta.9(zod@4.4.3) '@vercel/oidc': 3.2.0 - zod: 3.25.76 + zod: 4.4.3 - '@ai-sdk/gateway@4.0.0-canary.93(zod@3.25.76)': + '@ai-sdk/gateway@4.0.0-canary.93(zod@4.4.3)': dependencies: '@ai-sdk/provider': 4.0.0-canary.17 - '@ai-sdk/provider-utils': 5.0.0-canary.44(zod@3.25.76) + '@ai-sdk/provider-utils': 5.0.0-canary.44(zod@4.4.3) '@vercel/oidc': 3.2.0 - zod: 3.25.76 + zod: 4.4.3 - '@ai-sdk/openai@3.0.41(zod@3.25.76)': + '@ai-sdk/openai@3.0.41(zod@4.4.3)': dependencies: '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.29(zod@3.25.76) - zod: 3.25.76 + '@ai-sdk/provider-utils': 4.0.29(zod@4.4.3) + zod: 4.4.3 - '@ai-sdk/otel@1.0.0-beta.6(zod@3.25.76)': + '@ai-sdk/otel@1.0.0-beta.6(zod@4.4.3)': dependencies: '@ai-sdk/provider': 4.0.0-beta.5 '@opentelemetry/api': 1.9.0 - ai: 7.0.0-beta.60(zod@3.25.76) + ai: 7.0.0-beta.60(zod@4.4.3) transitivePeerDependencies: - zod - '@ai-sdk/provider-utils@1.0.22(zod@3.25.76)': + '@ai-sdk/provider-utils@1.0.22(zod@4.4.3)': dependencies: '@ai-sdk/provider': 0.0.26 eventsource-parser: 1.1.2 nanoid: 3.3.8 secure-json-parse: 2.7.0 optionalDependencies: - zod: 3.25.76 + zod: 4.4.3 - '@ai-sdk/provider-utils@4.0.29(zod@3.25.76)': + '@ai-sdk/provider-utils@4.0.29(zod@4.4.3)': dependencies: '@ai-sdk/provider': 3.0.10 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.1.0 - zod: 3.25.76 + zod: 4.4.3 - '@ai-sdk/provider-utils@5.0.0-beta.9(zod@3.25.76)': + '@ai-sdk/provider-utils@5.0.0-beta.9(zod@4.4.3)': dependencies: '@ai-sdk/provider': 4.0.0-beta.5 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.1.0 - zod: 3.25.76 + zod: 4.4.3 - '@ai-sdk/provider-utils@5.0.0-canary.44(zod@3.25.76)': + '@ai-sdk/provider-utils@5.0.0-canary.44(zod@4.4.3)': dependencies: '@ai-sdk/provider': 4.0.0-canary.17 '@standard-schema/spec': 1.1.0 '@workflow/serde': 4.1.0 eventsource-parser: 3.1.0 - zod: 3.25.76 + zod: 4.4.3 '@ai-sdk/provider@0.0.26': dependencies: @@ -15729,10 +15721,10 @@ snapshots: dependencies: json-schema: 0.4.0 - '@ai-sdk/react@3.0.170(react@18.3.1)(zod@3.25.76)': + '@ai-sdk/react@3.0.170(react@18.3.1)(zod@4.4.3)': dependencies: - '@ai-sdk/provider-utils': 4.0.29(zod@3.25.76) - ai: 6.0.116(zod@3.25.76) + '@ai-sdk/provider-utils': 4.0.29(zod@4.4.3) + ai: 6.0.116(zod@4.4.3) react: 18.3.1 swr: 2.2.5(react@18.3.1) throttleit: 2.1.0 @@ -17821,10 +17813,10 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@conform-to/zod@1.19.4(zod@3.25.76)': + '@conform-to/zod@1.19.4(zod@4.4.3)': dependencies: '@conform-to/dom': 1.19.4 - zod: 3.25.76 + zod: 4.4.3 '@connectrpc/connect-node@1.4.0(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.4.0(@bufbuild/protobuf@1.10.0))': dependencies: @@ -18899,7 +18891,7 @@ snapshots: '@microsoft/fetch-event-source@2.0.1': {} - '@modelcontextprotocol/sdk@1.25.2(hono@4.12.25)(supports-color@10.0.0)(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.25.2(hono@4.12.25)(supports-color@10.0.0)(zod@4.4.3)': dependencies: '@hono/node-server': 1.19.9(hono@4.12.25) ajv: 8.18.0 @@ -18915,13 +18907,13 @@ snapshots: json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 raw-body: 3.0.0 - zod: 3.25.76 - zod-to-json-schema: 3.25.0(zod@3.25.76) + zod: 4.4.3 + zod-to-json-schema: 3.25.0(zod@4.4.3) transitivePeerDependencies: - hono - supports-color - '@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.26.0(zod@4.4.3)': dependencies: '@hono/node-server': 1.19.9(hono@4.12.25) ajv: 8.18.0 @@ -18938,8 +18930,8 @@ snapshots: json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 raw-body: 3.0.0 - zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) + zod: 4.4.3 + zod-to-json-schema: 3.25.1(zod@4.4.3) transitivePeerDependencies: - supports-color @@ -22368,9 +22360,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@trigger.dev/platform@1.3.0(zod@3.25.76)': + '@trigger.dev/platform@1.3.1(zod@4.4.3)': dependencies: - zod: 3.25.76 + zod: 4.4.3 '@tybys/wasm-util@0.10.3': dependencies: @@ -22889,7 +22881,7 @@ snapshots: '@unkey/error@0.2.0': dependencies: - zod: 3.25.76 + zod: 4.4.3 '@upsetjs/venn.js@2.0.0': optionalDependencies: @@ -22961,8 +22953,8 @@ snapshots: '@vercel/sdk@1.19.1': dependencies: - '@modelcontextprotocol/sdk': 1.26.0(zod@3.25.76) - zod: 3.25.76 + '@modelcontextprotocol/sdk': 1.26.0(zod@4.4.3) + zod: 4.4.3 transitivePeerDependencies: - '@cfworker/json-schema' - supports-color @@ -23197,27 +23189,27 @@ snapshots: ahocorasick@1.0.2: {} - ai@6.0.116(zod@3.25.76): + ai@6.0.116(zod@4.4.3): dependencies: - '@ai-sdk/gateway': 3.0.66(zod@3.25.76) + '@ai-sdk/gateway': 3.0.66(zod@4.4.3) '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.29(zod@3.25.76) + '@ai-sdk/provider-utils': 4.0.29(zod@4.4.3) '@opentelemetry/api': 1.9.0 - zod: 3.25.76 + zod: 4.4.3 - ai@7.0.0-beta.60(zod@3.25.76): + ai@7.0.0-beta.60(zod@4.4.3): dependencies: - '@ai-sdk/gateway': 4.0.0-beta.30(zod@3.25.76) + '@ai-sdk/gateway': 4.0.0-beta.30(zod@4.4.3) '@ai-sdk/provider': 4.0.0-beta.5 - '@ai-sdk/provider-utils': 5.0.0-beta.9(zod@3.25.76) - zod: 3.25.76 + '@ai-sdk/provider-utils': 5.0.0-beta.9(zod@4.4.3) + zod: 4.4.3 - ai@7.0.0-canary.159(zod@3.25.76): + ai@7.0.0-canary.159(zod@4.4.3): dependencies: - '@ai-sdk/gateway': 4.0.0-canary.93(zod@3.25.76) + '@ai-sdk/gateway': 4.0.0-canary.93(zod@4.4.3) '@ai-sdk/provider': 4.0.0-canary.17 - '@ai-sdk/provider-utils': 5.0.0-canary.44(zod@3.25.76) - zod: 3.25.76 + '@ai-sdk/provider-utils': 5.0.0-canary.44(zod@4.4.3) + zod: 4.4.3 ajv-formats@2.1.1(ajv@8.20.0): optionalDependencies: @@ -23404,9 +23396,9 @@ snapshots: js-yaml: 4.1.1 linear-sum-assignment: 1.0.7 mustache: 4.2.0 - openai: 4.97.0(encoding@0.1.13)(ws@8.21.0(bufferutil@4.0.9))(zod@3.25.76) - zod: 3.25.76 - zod-to-json-schema: 3.24.5(zod@3.25.76) + openai: 4.97.0(encoding@0.1.13)(ws@8.21.0(bufferutil@4.0.9))(zod@4.4.3) + zod: 4.4.3 + zod-to-json-schema: 3.24.5(zod@4.4.3) transitivePeerDependencies: - encoding - ws @@ -25307,7 +25299,7 @@ snapshots: dependencies: require-like: 0.1.2 - evalite@1.0.0-beta.16(ai@6.0.116(zod@3.25.76))(better-sqlite3@11.10.0)(bufferutil@4.0.9): + evalite@1.0.0-beta.16(ai@6.0.116(zod@4.4.3))(better-sqlite3@11.10.0)(bufferutil@4.0.9): dependencies: '@fastify/static': 8.2.0 '@fastify/websocket': 11.2.0(bufferutil@4.0.9) @@ -25324,7 +25316,7 @@ snapshots: table: 6.9.0 tinyrainbow: 3.1.0 optionalDependencies: - ai: 6.0.116(zod@3.25.76) + ai: 6.0.116(zod@4.4.3) better-sqlite3: 11.10.0 transitivePeerDependencies: - bufferutil @@ -28219,7 +28211,7 @@ snapshots: transitivePeerDependencies: - encoding - openai@4.97.0(encoding@0.1.13)(ws@8.21.0(bufferutil@4.0.9))(zod@3.25.76): + openai@4.97.0(encoding@0.1.13)(ws@8.21.0(bufferutil@4.0.9))(zod@4.4.3): dependencies: '@types/node': 24.13.3 '@types/node-fetch': 2.6.12 @@ -28230,7 +28222,7 @@ snapshots: node-fetch: 2.6.12(encoding@0.1.13) optionalDependencies: ws: 8.21.0(bufferutil@4.0.9) - zod: 3.25.76 + zod: 4.4.3 transitivePeerDependencies: - encoding @@ -29561,7 +29553,7 @@ snapshots: '@remix-run/server-runtime': 2.17.5(typescript@6.0.3) react: 18.3.1 - remix-utils@7.7.0(@remix-run/node@2.17.5(typescript@6.0.3))(@remix-run/react@2.17.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@remix-run/router@1.23.3(patch_hash=33e2966e9ef36aa09955cec9922c5063227b83429157a71ba226369a08627ac9))(crypto-js@4.2.0)(intl-parse-accept-language@1.0.0)(react@18.3.1)(zod@3.25.76): + remix-utils@7.7.0(@remix-run/node@2.17.5(typescript@6.0.3))(@remix-run/react@2.17.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@6.0.3))(@remix-run/router@1.23.3(patch_hash=33e2966e9ef36aa09955cec9922c5063227b83429157a71ba226369a08627ac9))(crypto-js@4.2.0)(intl-parse-accept-language@1.0.0)(react@18.3.1)(zod@4.4.3): dependencies: type-fest: 4.33.0 optionalDependencies: @@ -29571,7 +29563,7 @@ snapshots: crypto-js: 4.2.0 intl-parse-accept-language: 1.0.0 react: 18.3.1 - zod: 3.25.76 + zod: 4.4.3 remove-accents@0.5.0: {} @@ -31686,31 +31678,25 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 - zod-error@1.5.0: + zod-error@2.0.0(zod@4.4.3): dependencies: - zod: 3.25.76 - - zod-to-json-schema@3.24.5(zod@3.25.76): - dependencies: - zod: 3.25.76 + zod: 4.4.3 - zod-to-json-schema@3.24.6(zod@3.25.76): + zod-to-json-schema@3.24.5(zod@4.4.3): dependencies: - zod: 3.25.76 + zod: 4.4.3 - zod-to-json-schema@3.25.0(zod@3.25.76): + zod-to-json-schema@3.25.0(zod@4.4.3): dependencies: - zod: 3.25.76 + zod: 4.4.3 - zod-to-json-schema@3.25.1(zod@3.25.76): + zod-to-json-schema@3.25.1(zod@4.4.3): dependencies: - zod: 3.25.76 + zod: 4.4.3 - zod-validation-error@1.5.0(zod@3.25.76): + zod-validation-error@5.0.0(zod@4.4.3): dependencies: - zod: 3.25.76 - - zod@3.25.76: {} + zod: 4.4.3 zod@4.4.3: {}