Validation ​

@sometic/validation provides a shared issue model, native validators, path helpers, composition utilities, and a schema-adapter contract without locking you into Zod, Yup, or Valibot.

Use it alone for value checks, or with @sometic/forms for form-wide validation.

Overview ​

ModuleImportPurpose
Issues / results@sometic/validationok, fail, createIssue, mergeResults, issuesForPath
Path helpers@sometic/validation/pathgetAt, setAt, deleteAt, parsePath, joinPath
Validators@sometic/validation/validatorsNative sync / async validators
Compose@sometic/validation/composepipe, all, any, when, refine, transform
Schema adapter@sometic/validation/schemaSchemaAdapter, fromSchema, assertSchemaAdapter
Define schema@sometic/validation/defineSmall first-party object/array schema builder
Zod adapter@sometic/validation-zodOptional peer wrapper (zod)
Yup adapter@sometic/validation-yupOptional peer wrapper (yup)

When to use ​

  • Shared validation logic across Vanilla controllers, forms, and adapters
  • Stable issue code strings for i18n
  • Bridging an external schema library through SchemaAdapter

When not to use ​

  • Full form lifecycle (drafts, submit, field registration) → @sometic/forms
  • HTML constraint validation alone may be enough for simple native forms
  • Do not put server authorization rules in client validators

Installation ​

pnpm
pnpm add @sometic/validation
npm
npm install @sometic/validation
yarn
yarn add @sometic/validation
bun
bun add @sometic/validation

Usage ​

Native validators ​

ts
import { required, email, minLength, runValidators } from "@sometic/validation/validators";

const result = await runValidators([required(), email(), minLength(5)], "a@b.co", {
    values: {},
    path: "email",
});

if (!result.valid) {
    console.log(result.issues);
}
js
import { required, email, minLength, runValidators } from "@sometic/validation/validators";

const result = await runValidators([required(), email(), minLength(5)], "a@b.co", {
    values: {},
    path: "email",
});

Compose ​

ts
import { pipe, when, all } from "@sometic/validation/compose";
import { required, min, max } from "@sometic/validation/validators";

const ageRules = pipe(
    required(),
    when((value) => value !== "", all([min(0), max(120)])),
);

Path helpers ​

ts
import { getAt, setAt, joinPath } from "@sometic/validation/path";

const values = { user: { email: "" } };
getAt(values, "user.email");
setAt(values, "user.email", "a@b.co");
joinPath("user", "email"); // "user.email"

Schema adapter seam ​

ts
import { fromSchema } from "@sometic/validation/schema";
import { defineSchema, object, string } from "@sometic/validation/define";

const adapter = defineSchema(
    object({
        email: string({ email: true, nonempty: true }),
    }),
);

const validator = fromSchema(adapter);

Zod / Yup (optional peers):

ts
import { z } from "zod";
import { createZodSchemaAdapter } from "@sometic/validation-zod";
import { fromSchema } from "@sometic/validation/schema";

const validator = fromSchema(createZodSchemaAdapter(z.string().email()));
ts
import * as yup from "yup";
import { createYupSchemaAdapter } from "@sometic/validation-yup";
import { fromSchema } from "@sometic/validation/schema";

const validator = fromSchema(createYupSchemaAdapter(yup.string().email().required()));

Key APIs ​

Built-in validators ​

required, minLength, maxLength, pattern, email, url, min, max, integer, oneOf, custom, plus runValidators and normalizeResult.

Issues ​

HelperRole
ok() / fail(issues)Build ValidationResult
createIssue({ code, message, path?, params? })Stable issue object
mergeResults / issuesForPathCombine / filter issues
debouncePromiseDebounce async validation with AbortSignal

Compose ​

pipe, all, any, when, refine, transform, syncOnly.

How it works ​

Validators return ValidationIssue objects (or results that normalize into them). runValidators fills path from context and supports async validators with optional AbortSignal. Forms depend on validation; validation never imports forms.

Schema libraries stay optional peers outside this package. SchemaAdapter is the extension point so Sometic never mandates Zod.

Edge cases ​

EdgeBehavior
Empty string + requiredFail with code required
Async abortHonor signal; do not apply stale results in forms
Custom codesPrefer stable strings (email, server, …) for i18n maps
Nested pathsUse path helpers; do not hand-roll string concat inconsistently

FAQ ​

Why not mandate Zod? ​

No schema-library lock-in. Native validators and @sometic/validation/define cover common cases. Prefer @sometic/validation-zod or @sometic/validation-yup when you already standardize on those libraries. SchemaAdapter + fromSchema is the shared seam.

Is define a Zod replacement? ​

No. define is a small object/array/string/number builder (tree-shakeable subpath). It is not Zod feature parity. Use Zod/Yup adapters when you need that ecosystem.

Can I use this without forms? ​

Yes. Forms depends on validation, not the reverse.

How do issue codes work? ​

Stable code strings plus human message and optional path / params for i18n.

Sync vs async? ​

Validators may return a Promise. runValidators always returns a Promise so callers can await uniformly.

Where is form-level docs? ​

Forms validation and Form component.