Skip to content
Helix Docs

Migrate to Zod 4

Use Zod 4 for Helix schemas: update the dependency, adjust the patterns that changed, bound untrusted arrays, and compose the SDK's exported schemas.

For developers Updated Oct 28, 2026
View as Markdown

Use Zod 4 for your Helix function and tool schemas. New projects from helix init scaffold it. Projects on Zod 3.25 or later keep working, so you can migrate when it suits you: update your zod dependency and adjust the few schema patterns that changed. Most schemas need no change.

Update the dependency

Set your project’s zod to ^4.6.6 and install:

npm install zod@^4.6.6

The SDK does not re-export zod, so your project declares its own, listed in package.json next to @trayai/helix-sdk. New projects already scaffold zod@^4.6.6.

Clear the security advisory

Zod versions before 4.6.6 carry a high-severity advisory, GHSA-mrhx-c7xc-m3w5. An array schema with no length limit records a validation issue for every failing element, so validating a very large input can exhaust memory and crash the process.

This is only reachable if one of your functions validates untrusted input with an unbounded z.array(...). Moving to 4.6.6 or later and bounding those arrays closes it:

import { z } from 'zod';

export const input = z.object({
  // Cap arrays that come from untrusted callers
  tags: z.array(z.string()).max(100),
});

The .max(...) bound is good practice on any Zod version.

Update your schemas

These are the Zod 3 patterns that change in Zod 4. Zod’s v4 changelog is the full reference.

  • z.record needs two arguments. z.record(valueSchema) becomes z.record(z.string(), valueSchema).
  • z.uuid() is stricter. It now accepts only standard 8-4-4-4-12 UUIDs. Use z.guid() for other hex IDs.
  • ZodError.errors is removed. Read .issues instead.
  • .default() short-circuits. When the input is undefined, the default is returned without re-parsing. Use .prefault() where you want the default parsed.
  • Top-level string formats. Prefer z.email(), z.url(), and z.uuid(). The method forms like z.string().email() still work but are deprecated.
  • z.unknown() and z.any() keys are now required. An object key whose value is z.unknown() or z.any() must be present at parse time. Add .optional() to keep the old behavior.

Compose the SDK’s exported schemas

The SDK exports a few schemas for validating Helix identifiers. On Zod 4, compose these into your own object schemas:

ExportValidates
region.schemaA Helix region name.
projectIdSchemaA project ID.
workspaceIdSchemaA workspace ID.
import { z } from 'zod';
import { projectIdSchema, region } from '@trayai/helix-sdk';

export const input = z.object({
  projectId: projectIdSchema,
  region:    region.schema,
});

These are Zod 4 schemas, so you nest them inside your Zod 4 objects. You can also call .parse() or .safeParse() on any of them directly. The region export also provides region.values, the region names Helix accepts, and region.isKnown(name), which returns true for an accepted name. Neither needs Zod.

Automate it with an AI assistant

If you build with Claude Code or another coding agent, this prompt migrates a project’s own schemas:

Upgrade this project from Zod 3 to Zod 4.

1. Set "zod" to "^4.6.6" in package.json and install.
2. Update my Zod schemas for Zod 4:
   - z.record(value) -> z.record(z.string(), value)
   - z.string().uuid() -> z.uuid(); use z.guid() for non-RFC hex IDs
   - ZodError.errors -> .issues
   - .default() that should re-parse -> .prefault()
   - z.string().email()/.url()/.uuid() -> z.email()/z.url()/z.uuid()
   - add .optional() to any z.unknown()/z.any() object key that should stay optional
3. Add .max(...) to any z.array() that validates untrusted input.
4. Run the type checker and tests, and fix anything that breaks.

Leave schemas that already work unchanged.

Verify the upgrade

Run your type checker and tests, and fix anything that breaks:

npm run typecheck
npm test

Then run helix dev and exercise a function that validates input, so you catch any schema that parses differently under Zod 4.

Loading search…

Jump to a section

tab to move · esc to close