> ## Documentation Index
> Fetch the complete documentation index at: https://next-safe-env.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Use Zod schemas with next-safe-env

> Pass z.object() schemas directly to createEnv if your project already uses Zod. No peer dependency required - next-safe-env duck-types Zod at runtime.

If your project already uses Zod, you can pass `z.object()` schemas directly to `createEnv` instead of rewriting your validation logic using `next-safe-env`'s native validators. There is no peer dependency requirement - `next-safe-env` detects Zod schemas at runtime via structural typing (duck-typing), so installing `next-safe-env` will not pull Zod into projects that do not already have it.

## Complete example

```typescript theme={null}
import { z } from 'zod'
import { createEnv } from 'next-safe-env'

export const env = createEnv({
  server: z.object({
    DATABASE_URL: z.string().url(),
    PORT:         z.coerce.number().int().min(1).max(65535).default(3000),
    NODE_ENV:     z.enum(['development', 'production', 'test']).default('development'),
  }),
  client: z.object({
    NEXT_PUBLIC_APP_NAME: z.string().default('My App'),
  }),
  runtimeEnv: {
    DATABASE_URL:         process.env.DATABASE_URL,
    PORT:                 process.env.PORT,
    NODE_ENV:             process.env.NODE_ENV,
    NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
  },
})
```

## Mixing native and Zod schemas

You can use a Zod schema for one side of the split and native `next-safe-env` validators for the other. This is useful when you want to migrate incrementally or when the two schemas are maintained by different parts of your team:

```typescript theme={null}
import { z } from 'zod'
import { createEnv, str, url } from 'next-safe-env'

export const env = createEnv({
  server: z.object({
    DATABASE_URL: z.string().url(),
    NODE_ENV:     z.enum(['development', 'production', 'test']),
  }),
  client: {
    NEXT_PUBLIC_API_URL:  url(),
    NEXT_PUBLIC_APP_NAME: str().default('My App'),
  },
  runtimeEnv: {
    DATABASE_URL:         process.env.DATABASE_URL,
    NODE_ENV:             process.env.NODE_ENV,
    NEXT_PUBLIC_API_URL:  process.env.NEXT_PUBLIC_API_URL,
    NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
  },
})
```

## What works with Zod

All standard Zod features work end-to-end when passed through `createEnv`:

* **Coercions** - `z.coerce.number()` parses the raw string value into a number, the same way `num()` does natively.
* **Transforms** - `z.string().transform(v => v.trim())` runs after parsing and its output type is reflected in the inferred return type.
* **Defaults** - `z.string().default('My App')` is applied when the variable is `undefined`, the same as `.default('My App')` on a native validator.
* **Enums** - `z.enum(['development', 'production', 'test'])` narrows the inferred type to the literal union.

## Error output

When a Zod field fails validation, `next-safe-env` maps the Zod error into the same `ValidationFailure` shape it uses for native validator failures. All errors appear together in the same pretty-printed output:

```
[next-safe-env] Environment validation failed - 2 error(s):

  ✗ DATABASE_URL  - Required. Expected a valid URL. Got: "localhost"
  ✗ PORT          - Expected number, received nan

  Server vars:  1 valid, 2 invalid
  Client vars:  1 valid, 0 invalid

  Set the correct values in your .env file or deployment environment and restart.
```

You do not need to handle Zod errors separately - they surface through the same `onValidationError` callback and the same exit path as native validation failures.

<Note>
  All adapter rules still apply when you use Zod schemas. If you are using the `nextjs` adapter, every key in `client` must be prefixed with `NEXT_PUBLIC_`. The adapter enforces this rule against the schema keys regardless of whether the schema is native or Zod-based. Server vars are still stripped from the returned object in browser context.
</Note>

<Note>
  You still need Zod installed in your own project - `next-safe-env` will not install it for you. The "no peer dependency" guarantee means `next-safe-env` will not break if you do not have Zod installed; it simply will not recognize non-Zod objects as Zod schemas.
</Note>
