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

@umpire/zod

v1.0.1

Published

Availability-aware Zod validation for [@umpire/core](https://www.npmjs.com/package/@umpire/core)-powered state. Disabled fields produce no validation errors. Required/optional follows Umpire's availability map.

Downloads

333

Readme

@umpire/zod

Availability-aware Zod validation for @umpire/core-powered state. Disabled fields produce no validation errors. Required/optional follows Umpire's availability map.

Docs · Quick Start

Install

npm install @umpire/core @umpire/zod zod

zod is a peer dependency — bring your own version (v3 or v4).

Usage

import { z } from 'zod'
import { umpire, enabledWhen, requires } from '@umpire/core'
import {
  createZodAdapter,
  deriveErrors,
  deriveSchema,
  zodErrors,
} from '@umpire/zod'

// 1. Define availability rules
const ump = umpire({
  fields: {
    email: { required: true, isEmpty: (v) => !v },
    companyName: { required: true, isEmpty: (v) => !v },
  },
  rules: [
    enabledWhen('companyName', (_v, c) => c.plan === 'business', {
      reason: 'business plan required',
    }),
  ],
})

// 2. Define per-field Zod schemas
const fieldSchemas = {
  email: z.string().email('Enter a valid email'),
  companyName: z.string().min(1, 'Company name is required'),
}

// 3. Compose at render time
const availability = ump.check(values, { plan })

const schema = deriveSchema(availability, fieldSchemas)
const result = schema.safeParse(values)

if (!result.success) {
  const errors = deriveErrors(availability, zodErrors(result.error))
  // errors.email → 'Enter a valid email' (only if email is enabled)
  // errors.companyName → undefined (disabled on personal plan)
}

const validation = createZodAdapter({
  schemas: fieldSchemas,
})

const umpWithValidation = umpire({
  fields: {
    email: { required: true, isEmpty: (v) => !v },
    companyName: { required: true, isEmpty: (v) => !v },
  },
  rules: [
    enabledWhen('companyName', (_v, c) => c.plan === 'business', {
      reason: 'business plan required',
    }),
  ],
  validators: validation.validators,
})

API

deriveSchema(availability, schemas)

Builds a z.object() from the availability map:

  • Disabled fields are excluded entirely
  • Enabled + required fields use the base schema
  • Enabled + optional fields get .optional()

Pass per-field schemas directly, or use formSchema.shape to extract from an existing z.object().

Throws if you accidentally pass a z.object() instead of its .shape — the error message tells you what to do.

deriveErrors(availability, errors)

Filters normalized field errors to only include enabled fields and keeps the first message per field. Returns Partial<Record<field, message>>.

zodErrors(error)

Normalizes a Zod error's issues array into { field, message }[] pairs for use with deriveErrors.

createZodAdapter({ schemas, build? })

Creates a convenience adapter with:

  • validators for umpire({ validators }), surfacing the first field-level Zod issue as error
  • run(availability, values) for the full deriveSchema() -> safeParse() -> deriveErrors() flow

If you need every issue or deeper control, you can still use deriveSchema() and safeParse() directly.

Blank strings and isEmpty

The generated validators follow Umpire's satisfaction semantics. By default, only null and undefined count as empty. So if a string field does not define isEmpty, a value like '' is still considered satisfied and may surface valid: false immediately.

For form-style inputs, define an explicit empty-state rule:

import { isEmptyString, umpire } from '@umpire/core'

const validation = createZodAdapter({
  schemas: {
    email: z.string().email('Enter a valid email'),
  },
})

const ump = umpire({
  fields: {
    email: { required: true, isEmpty: isEmptyString },
  },
  rules: [],
  validators: validation.validators,
})

That keeps blank strings out of the validation path until the field is satisfied under your chosen emptiness semantics.

Devtools

If you use @umpire/devtools, @umpire/zod/devtools can expose validation state in a tab. The most ergonomic path is to derive from the current devtools context:

import { useUmpireWithDevtools } from '@umpire/devtools/react'
import { zodValidationExtension } from '@umpire/zod/devtools'

const validation = createZodAdapter({
  schemas: fieldSchemas,
  build(baseSchema) {
    return baseSchema.refine(
      (data) =>
        !data.confirmPassword ||
        !data.password ||
        data.confirmPassword === data.password,
      { message: 'Passwords do not match', path: ['confirmPassword'] },
    )
  },
})

const { check } = useUmpireWithDevtools('signup', ump, values, conditions, {
  extensions: [
    zodValidationExtension({
      resolve({ scorecard, values }) {
        return validation.run(scorecard.check, values)
      },
    }),
  ],
})

If you already have a precomputed parse result, the helper also accepts availability, result, and optional schemaFields directly.

The first pass shows:

  • valid/invalid
  • surfaced error count
  • suppressed and unmapped issue counts
  • the derived error map after availability filtering
  • optional derived schema field names

It does not currently detect skipped refine()/superRefine() execution on its own. If we want that, we will likely need richer validation instrumentation than a plain safeParse() result exposes.

The Render Loop

The payoff — one loop renders every field regardless of availability, validation, or fouls:

{
  fields.map((field) => {
    const av = availability[field]
    const error = validationErrors[field]

    return (
      <div className={av.enabled ? '' : 'disabled'}>
        <input disabled={!av.enabled} value={values[field]} />
        {!av.enabled && <span>{av.reason}</span>}
        {av.enabled && error && <span className="error">{error}</span>}
      </div>
    )
  })
}

No per-field branching. No if (field === 'companyName' && plan === 'business'). The rules and schemas declare everything upfront. The render loop just reads.

Docs