npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@validup/zod

v0.3.3

Published

A validup integration for zod.

Downloads

2,776

Readme

@validup/zod 🛡️

npm version Master Workflow CodeQL Known Vulnerabilities Conventional Commits

A validup integration for zod — turn any zod schema into a validup Validator.

Wrap any zod schema as a validup Validator, mount it on a Container path, and let validup orchestrate path expansion, group filtering, and error aggregation while zod does the actual schema parsing.

ℹ️ Already on zod 3.24+ and don't need vendor-specific fields?

@validup/standard-schema covers zod via the Standard Schema protocol and works the same against valibot, arktype, and effect-schema. Pick @validup/zod when you specifically need expected/received on issues or the bidirectional validup ↔ zod mapping.

Table of Contents

Installation

npm install @validup/zod validup zod --save

| Peer dependency | Supported versions | |-----------------|---------------------| | validup | ^1.0.0 | | zod | ^4.0.0 |

ℹ️ zod 3 support was dropped in @validup/[email protected]. The adapter's ZodIssue type alias resolves to zod/v4/core's $ZodRawIssue, which is not available in zod 3. Stay on @validup/zod@<1 if you cannot upgrade zod; otherwise upgrade zod to ^4.0.0 alongside this package.

Quick Start

import { Container, ValidupError } from 'validup';
import { createValidator } from '@validup/zod';
import { z } from 'zod';

const user = new Container<{ email: string; age: number }>();

user.mount('email', createValidator(z.string().email()));
user.mount('age',   createValidator(z.number().int().positive()));

try {
    const valid = await user.run({
        email: '[email protected]',
        age: 28,
    });
    // valid is { email, age }
} catch (error) {
    if (error instanceof ValidupError) {
        console.log(error.issues);
        // [{ type: 'item', path: ['email'], message: 'Invalid email address', ... }]
    }
}

createValidator returns a validup ValidatorDescriptor — interchangeable with a bare Validator at the mount site. Mount it like any other validator:

container.mount('field', { group: 'create' }, createValidator(z.string()));
container.mount('opt',   { optional: true },  createValidator(z.number()));

Per-Context Schemas

Pass a function instead of a schema to build the schema lazily from the validator context (e.g. depending on the active group, sibling values, or path):

import { createValidator } from '@validup/zod';
import { z } from 'zod';

const passwordValidator = createValidator((ctx) => {
    if (ctx.group === 'create') {
        return z.string().min(12);   // strict for new users
    }
    return z.string().min(12).optional(); // lenient on update
});

container.mount('password', { group: 'create' }, passwordValidator);
container.mount('password', { group: 'update' }, passwordValidator);

The factory receives the full ValidatorContext:

type ZodCreateFn<C = unknown> = (ctx: ValidatorContext<C>) => ZodType;

createValidator<C>(...) is generic over the validup context type, so factories can read typed ctx.context when the parent container declares one (Container<T, C>).

Result Caching

createValidator returns a ValidatorDescriptor (interchangeable with a bare Validator at the mount site). It participates in validup's result cache by default — most zod schemas (z.string().email(), length / regex / enum) are deterministic, so cached (value, context, group) snapshots replay without re-running the schema.

container.mount('email', createValidator(z.string().email()));                              // cached
container.mount('email', createValidator(asyncZodSchema, { sideEffect: true }));            // never cached

Pass { sideEffect: true } for schemas with async refines or superRefine calls reading external state — the framework will then re-run them on every invocation, ignoring any cached entry.

Error Mapping

Zod → Validup

When a schema fails to parse, the adapter calls safeParseAsync, then converts each ZodIssue into a validup IssueItem with the original path, message, and (when present) expected / received fields. The adapter then throws a ValidupError containing those issues.

Each zod issue's code is mapped onto validup's IssueCode vocabulary so consumer-side i18n catalogs (e.g. @ilingo/validup) can ship one parameterized message per code instead of falling back to a generic "invalid value" string:

| Zod issue | Validup IssueCode | data | |--------------------------------------------------------|------------------------------------|---------------------| | invalid_type (input at path is undefined) | REQUIRED | — | | invalid_type (wrong type) | VALUE_INVALID | — | | too_small, origin string / array / set / file| MIN_LENGTH | { min: number } | | too_big, origin string / array / set / file | MAX_LENGTH | { max: number } | | too_small, origin number / bigint / date / int | MIN_VALUE | { min: number } | | too_big, origin number / bigint / date / int | MAX_VALUE | { max: number } | | invalid_format, format email | EMAIL | — | | invalid_format, format url | URL | — | | invalid_format, format uuid / guid | UUID | — | | invalid_format, format regex | PATTERN | { pattern: string } | | invalid_format, format date / time / datetime / duration | DATE | — | | invalid_format, format ipv4 / ipv6 / cidrv4 / cidrv6 | IP_ADDRESS | — | | invalid_format, format base64 / base64url | BASE64 | — | | invalid_format, format json_string | JSON | — | | invalid_value (enum / literal mismatch) | ONE_OF_FAILED | — | | Everything else (custom, not_multiple_of, unrecognized_keys, invalid_union, …) | VALUE_INVALID | — |

ℹ️ REQUIRED detection requires the input. Zod 4 strips received / input from the formatted ZodError, so the adapter recovers the missing-key signal by looking the issue path up against the original parsed value. createValidator threads ctx.value through automatically; if you call buildIssuesForZodError(error) directly without a second argument, missing keys stay on VALUE_INVALID. Pass the input explicitly (buildIssuesForZodError(error, input)) to opt in.

container.mount('user', createValidator(z.object({
    name: z.string(),
    age: z.number().min(18),
})));

try {
    await container.run({ user: { name: 42, age: 10 } });
} catch (error) {
    if (error instanceof ValidupError) {
        // error.issues:
        // [
        //   { type: 'group', path: ['user'], issues: [
        //     { type: 'item', path: ['user', 'name'], message: 'Expected string, received number', ... },
        //     { type: 'item', path: ['user', 'age'],  message: 'Number must be greater than or equal to 18', ... },
        //   ]}
        // ]
    }
}

Paths from the wrapped zod schema are merged with the parent container's path, so deeply nested validation produces a fully-qualified path array on every issue.

Validup → Zod

Going the other way, you can convert a ValidupError (or a single Issue) into zod's raw issue format — useful when integrating validup output into zod-driven pipelines such as form libraries:

import { buildZodIssuesForError, buildZodIssuesForIssue } from '@validup/zod';

try {
    await container.run(input);
} catch (error) {
    if (error instanceof ValidupError) {
        const zodIssues = buildZodIssuesForError(error);
        // [{ code: 'custom', message: '...', path: [...], input: ... }, ...]
    }
}

⚠️ IssueGroup round-trip is lossy. validup's Issue is a discriminated union of IssueItem and IssueGroup. zod has no group equivalent, so buildZodIssuesForIssue recursively flattens every IssueGroup into its constituent IssueItems when emitting zod issues — group-level metadata (code: 'one_of_failed', data.name, etc.) is dropped. The reverse direction (zod → validup) never produces groups, so a zod ← validup round-trip on a flat issue list is preserved; a round-trip that involves grouped errors is not.

API Reference

| Export | Description | |-------------------------------------|------------------------------------------------------------------------------| | createValidator(schema, options?) | Wrap a ZodType (or (ctx) => ZodType) as a validup ValidatorDescriptor. options.sideEffect: true bypasses the result cache (use for async refines / superRefine reading external state). | | buildIssuesForZodError(e, input?) | Convert a ZodError into an array of validup Issues. Pass the parsed input as the second argument to enable invalid_typeREQUIRED promotion for missing keys. | | buildZodIssuesForError(e) | Convert a ValidupError into an array of zod raw issues. | | buildZodIssuesForIssue(i) | Convert a single validup Issue into zod raw issues (recurses into groups). | | ZodIssue | Re-exported alias for $ZodRawIssue from zod/v4/core. |

function createValidator<C = unknown, Z extends ZodType = ZodType>(
    input: Z | ((ctx: ValidatorContext<C>) => Z),
    options?: { sideEffect?: boolean },
): ValidatorDescriptor<C, ZodOutput<Z>>;

Stability

What's covered by semver:

  • Public exportscreateValidator, buildIssuesForZodError, buildZodIssuesForError, buildZodIssuesForIssue, and the ZodIssue type alias.
  • Error-mapping shapeIssueItem.path mirrors the failing zod path; IssueItem.code is mapped onto the validup vocabulary (min_length, max_length, min_value, max_value, email, url, uuid, pattern, date, ip_address, base64, json, required, one_of_failed, …) so consumer-side i18n catalogs can ship one parameterized message per code; unmapped zod codes fall back to value_invalid. IssueItem.expected / received are passed through when zod exposes them. buildZodIssuesForError reconstructs a zod-shaped representation from a ValidupError (code: 'custom', the message, the path, and the original received value as input) — it does not preserve the round-tripped vocabulary code. If you need the original zod codes / vendor fields end-to-end, keep the ZodError available alongside the ValidupError.
  • Per-context schema factory(ctx: ValidatorContext<C>) => ZodType invocation contract.

Known lossy behavior:

  • buildZodIssuesForIssue flattens IssueGroups when emitting zod issues (zod has no group concept) — group-level code and data are dropped.

Peer dependency policy:

  • validup ^1.0.0, zod ^4.0.0. zod 3.x was dropped in @validup/[email protected] because the adapter's ZodIssue type alias resolves to zod/v4/core, which is not available in zod 3.

Deprecation policy: matches validup — at least one minor release of @deprecated notice before removal in a major.

License

Made with 💚

Published under Apache 2.0 License.