Skip to content
spell logoSpellValidation
Schema validation with explicit sync/async checks, portable definitions, JSON Schema export, and tree-shakeable entry points.
Version
v2.1.1
Size
10.7 KB gzip
BrowserNode ≥22SSRDeno
sSchemaPipeSchemaSpellValidationErrorSpellDefinitionError View all 9 exports

Why Spell?

Spell keeps runtime validation, static inference, and portable definitions in one API. Use s for schema construction; import JSON conversion and predicates from dedicated subpaths.

This example shows the difference between manual branching and a single reusable schema.

ts
// Before
function parseUserBefore(value: unknown) {
  if (typeof value !== 'object' || value === null) throw new Error('Expected object');

  const candidate = value as Record<string, unknown>;

  if (typeof candidate.email !== 'string' || !candidate.email.includes('@')) {
    throw new Error('Expected valid email');
  }

  if (typeof candidate.role !== 'string' || !['admin', 'editor', 'viewer'].includes(candidate.role)) {
    throw new Error('Expected valid role');
  }

  return {
    email: candidate.email,
    role: candidate.role,
  };
}

// After
import { s } from '@vielzeug/spell';

const User = s.object({
  email: s.string().email(),
  role: s.enum(['admin', 'editor', 'viewer'] as const),
});

const user = User.parse({ email: 'ada@example.com', role: 'admin' });
FeatureSpellZodYup
Bundle size10.7 KB~62 kB~14 kB
Type inference Infer<T>Partial
Coercion API s.coerce.*
Async validation .checkAsync()
Error flattening flatten() + flattenFirst()Partial
Zero dependencies

Use Spell when you want a fluent schema API with strong TypeScript inference, structured errors, and no third-party runtime dependencies.

Consider alternatives when you are already standardized on another validator ecosystem and migration cost outweighs the API benefits.

Installation

Use your workspace package manager to add Spell.

sh
pnpm add @vielzeug/spell
sh
npm install @vielzeug/spell
sh
yarn add @vielzeug/spell

Quick Start

Start with a schema, then parse unknown input and use the inferred output type everywhere else.

ts
import { s, type Infer } from '@vielzeug/spell';

const User = s
  .object({
    email: s.string().email(),
    name: s.string().min(1),
    role: s.enum(['admin', 'editor', 'viewer'] as const),
  })
  .relaxed(); // allow extra keys — omit for strict-mode (default)

type User = Infer<typeof User>;

const payload: unknown = {
  email: 'ada@example.com',
  name: 'Ada',
  role: 'admin',
  team: 'platform',
};

const result = User.safeParse(payload);

if (!result.success) throw result.error;
const user = result.data;

Features

  • Namespace and tree-shakeable schema builders.
  • Sync and async parsing with parse(), safeParse(), parseAsync(), and safeParseAsync().
  • Explicit check() and checkAsync() rules; sync parsing never skips an async check.
  • Wrapper modes for optional, nullable, nullish, default, catch, and required.
  • Frozen declarative definitions through definition() and JSON Schema export via fromDefinition() from @vielzeug/spell/json.
  • Grouped diagnostics and predicates utilities keep schema construction focused.
  • Ordered union parsing produces the same selected branch in sync and async modes.
  • Structured errors with direct path lookup, flattened views, and best-match union diagnostics.
  • Object parsing is hardened against prototype-pollution-style keys.

Documentation

See Also

  • Forge — typed form state that uses Spell schemas as its validation layer
  • Courier — HTTP client for validating request and response payloads at service boundaries
  • Vault — unified storage API that accepts Spell schemas to type-gate persisted data