# 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.

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:

```bash
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`.

:::roadmap{title="Zod 3 is being phased out"}
Zod 4 is the recommended version. Support for Zod 3 is planned for removal in a future major version of the SDK. Projects on Zod 3.25 or later keep working today.
:::

## Clear the security advisory

Zod versions before `4.6.6` carry a high-severity advisory, [GHSA-mrhx-c7xc-m3w5](https://github.com/advisories/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:

```typescript
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](https://zod.dev/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:

| Export | Validates |
|---|---|
| `region.schema` | A Helix region name. |
| `projectIdSchema` | A project ID. |
| `workspaceIdSchema` | A workspace ID. |

```typescript
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.

:::note
The `regionSchema` export is a Zod 3 schema, kept for projects still on Zod 3. Compose it into a Zod 3 object, and once you migrate, use `region.schema` instead.
:::

## Automate it with an AI assistant

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

```text
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:

```bash
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.

---

Canonical: https://helix.tray.ai/documentation/guides/migrating-to-zod-4/
Any link on this page is available as markdown by appending .md to its URL.
Full corpus: https://helix.tray.ai/documentation/llms-full.txt