Integrations Overview
validup keeps a pure-JS core. Integration packages bridge an existing validation library or a host framework into the Container / Validator / Issue model.
Two shapes
Validator adapters expose a createValidator() function that returns a validup Validator:
import { createValidator } from '@validup/zod';
import { z } from 'zod';
container.mount('email', createValidator(z.string().email()));These come in three flavors today:
| Package | Wraps | Pick when… |
|---|---|---|
@validup/standard-schema | Any Standard Schema library (zod 3.24+, valibot, arktype, …) | You want vendor-neutral schemas |
@validup/zod | zod v4 | You need vendor-specific issue fields (expected, received) |
@validup/validator-js | validator.js string validators (isEmail, isLength, isInt, …) | You want pre-baked factories for the common rules with vocabulary codes baked in |
All three adapters name that entry point createValidator on purpose — same role, same contract, one thing to learn. The symmetry costs you an alias when two of them (or an adapter and the library it wraps) meet in one file.
Combining adapters in one file
@validup/validator-js names its factories after the validator.js rules they wrap — isEmail, isURL, isUUID, isInt, equals, matches, isDate — so the collision to plan for is usually with validator.js itself (reached for directly for the long tail: isCreditCard, isJWT, …), not just with a sibling adapter.
A namespace import resolves every name at once and reads better than aliasing them one by one:
import * as vjs from '@validup/validator-js';
import validator from 'validator';
import { createValidator as createZod } from '@validup/zod';
import { z } from 'zod';
// zod for schema-shaped fields …
container.mount('email', createZod(z.string().email()));
// … validator-js factories for vocabulary rules, with issue codes baked in
container.mount('passwordConfirm', vjs.equals('password'));
// … and validator.js directly for anything not pre-baked
container.mount('card', vjs.createValidator(validator.isCreditCard, { code: 'credit_card' }));Aliasing individual imports works too, and is the lighter touch when you only need one or two symbols:
import { createValidator as createZod } from '@validup/zod';
import { createValidator as createStandard } from '@validup/standard-schema';Framework / runtime integrations consume a whole Container<T, C> and wire it into a host environment:
| Package | Host |
|---|---|
@validup/vue | Vue 3 reactive forms |
Writing your own
If you want to bridge another library, copy the shape of @validup/zod:
import { type Validator, type ValidatorContext, ValidupError } from 'validup';
type CreateFn<C> = (ctx: ValidatorContext<C>) => Schema;
export function createValidator<C = unknown>(input: Schema | CreateFn<C>): Validator<C> {
return async (ctx): Promise<unknown> => {
const schema = typeof input === 'function' ? input(ctx) : input;
const outcome = await schema.safeParseAsync(ctx.value);
if (outcome.success) return outcome.data;
throw new ValidupError(buildIssuesForError(outcome.error));
};
}Three contract points to preserve:
- Accept either a schema or a
(ctx) => schemafactory. - Make
createValidator<C>generic over the validup context. - Translate foreign errors into
Issue[]usingdefineIssueItem/defineIssueGroup— never construct issue objects literally.
For framework integrations, study @validup/vue — a reactive composable that drives form state from safeRun().