Skip to content
spell logoSpellValidation
Zero-dependency schema validation with explicit async checks, Standard Schema interoperability, and JSON Schema export.
Version
v3.1.1
Size
10.4 KB gzip
BrowserNode ≥22SSRDeno
screateParseContextjoinIssuePathSpellValidationErrorSpellDefinitionError 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.4 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().
  • Dependency-free Standard Schema v1 interoperability through schema['~standard'].
  • 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.
  • Request-local message customization through createParseContext().
  • Ordered union parsing produces the same selected branch in sync and async modes.
  • Structured errors with direct path lookup and flattened views.
  • 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