Why Spell?
Spell keeps runtime validation, static inference, and schema introspection in one API. You can use the namespace form for ergonomics or the sXxx exports for tree shaking. Descriptor and JSON Schema output make Spell useful at API boundaries, build tooling, and documentation layers.
This example shows the difference between manual branching and a single reusable schema.
// 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' });| Feature | Spell | Zod | Yup |
|---|---|---|---|
| Bundle size | 12.0 KB | ~62 kB | ~14 kB |
| Type inference | Infer<T> | Partial | |
| Coercion API | s.coerce.* | ||
| Async validation | .validate() | ||
| Error flattening | flatten() + flattenFirst() | Partial | |
| Zero dependencies |
Use Spell when you want a fluent schema API with strong TypeScript inference, structured errors, and zero 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.
pnpm add @vielzeug/spellnpm install @vielzeug/spellyarn add @vielzeug/spellQuick Start
Start with a schema, then parse unknown input and use the inferred output type everywhere else.
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 user = User.parse(payload);Features
- Namespace and tree-shakeable schema builders.
- Sync and async parsing with
parse(),safeParse(),parseAsync(), andsafeParseAsync(). - Unified
validate()for both sync and async custom rules; boolean/string shorthand supported. - Wrapper modes for
optional,nullable,nullish,default,catch, andrequired. - Descriptor serialization with
toDescriptor()and JSON Schema export viadescriptorToJsonSchema(). - Message overrides via
setMessages()and logger routing viasetLogger(). - Standalone validators for sizes, numeric ranges, and common string formats.
- Structured errors with flattened, formatted, and best-match union diagnostics.
- Object parsing and error formatting hardened against prototype-pollution-style keys.