Skip to content

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:

typescript
import { createValidator } from '@validup/zod';
import { z } from 'zod';

container.mount('email', createValidator(z.string().email()));

These come in three flavors today:

PackageWrapsPick when…
@validup/standard-schemaAny Standard Schema library (zod 3.24+, valibot, arktype, …)You want vendor-neutral schemas
@validup/zodzod v4You need vendor-specific issue fields (expected, received)
@validup/validator-jsvalidator.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:

typescript
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:

typescript
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:

PackageHost
@validup/vueVue 3 reactive forms

Writing your own

If you want to bridge another library, copy the shape of @validup/zod:

typescript
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:

  1. Accept either a schema or a (ctx) => schema factory.
  2. Make createValidator<C> generic over the validup context.
  3. Translate foreign errors into Issue[] using defineIssueItem / defineIssueGroup — never construct issue objects literally.

For framework integrations, study @validup/vue — a reactive composable that drives form state from safeRun().

Released under the Apache 2.0 License.