mrkeyoor.com_
Sun 20 Sept 12:45 UTC
npmUtilsupdated 20 Sept 2026

valibot review

Valibot 1.4.2 validates unknown JavaScript values and derives TypeScript input and output types from the same schema. It composes small schema and action functions with `pipe()`: `parse()` throws, `safeParse()` returns issues, and `is()` narrows a value. Object constructors choose whether extra keys are removed, rejected, preserved, or validated by a rest schema. The current patch caches `Intl.Segmenter` during word-count checks and fixes `flatten()` and `intersect()` for keys such as `toString` that collide with `Object.prototype`. Valibot also implements Standard Schema for framework integration.

Verdict

Valibot 1.4.2 installed in 1.1 seconds as one 2 MB package with zero direct dependencies and no audit findings; our full namespace import bundled to 14.6 KB gzipped. It is a strong browser-facing choice when schemas use static imports and Standard Schema, while Zod remains the safer pick for dependencies that demand Zod itself or teams committed to fluent methods.

We installed it

Lab card: what happened when we installed valibotScreenshot of valibot documentation
Install✓ · 1.1s1 package on disk · 2 MB
ImportESM import works · require() works · ESM package with exports map
Browser14.6 KBgzipped (83.8 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does valibot install cleanly?

Yes. In a fresh container with an empty cache, npm install valibot finished in 1 seconds, leaving 1 package and 2 MB on disk. npm audit reported no known vulnerabilities.

How much does valibot add to a browser bundle?

14.6 KB gzipped (83.8 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does valibot work with both ESM and CommonJS?

Yes. Both import 'valibot' and require('valibot') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does valibot include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

valibot or zod: which should you use?

zod: Use it when chained methods, community examples, and integrations that explicitly require Zod outweigh client bundle concerns. Valibot 1.4.2 installed in 1.1 seconds as one 2 MB package with zero direct dependencies and no audit findings; our full namespace import bundled to 14.6 KB gzipped.

When should you not use valibot?

The project is untyped JavaScript with async validation. TypeScript catches sync parser and async schema mismatches that plain JavaScript can let through incorrectly.

API stability4/5The 1.x line keeps the function-and-pipe model, sync and async parser split, inferred input and output types, and explicit object modes. Version 1.4.2 fixes word checks, flattened issues, and intersections without changing ordinary schema syntax. The score is 4 because pre-1.0 examples remain searchable and companion packages for JSON Schema or internationalization have independent releases that must be matched during upgrades.
Docs5/5valibot.dev provides task guides, a reference for each schema and action, parsing and issue recipes, object-mode explanations, async examples, Standard Schema notes, and a Zod migration guide. Examples distinguish `InferInput` from `InferOutput` and show how paths reach form fields. The main risk is distributed across several pages: plain JavaScript receives no compiler error when a synchronous parser is paired with an asynchronous schema.
Maintenance4/5GitHub shows an unarchived repository pushed on August 22, 2026, with 173 open issues and pull requests and 8,960 stars. Version 1.4.2 shipped on June 28 with fixes tied to named pull requests, including prototype-collision bugs and repeated `Intl.Segmenter` construction. Current work is evident, while the visible queue and separately versioned companion packages still argue for pinned versions and representative invalid-input tests.
Ecosystem4/5npm recorded 17,987,920 downloads for the week ending August 24, 2026. Standard Schema gives frameworks a vendor-neutral validator interface, and the package includes TypeScript declarations plus working ESM and CommonJS loading in our check. Zod retains more tutorials and consumers that inspect its concrete schema classes, so Valibot's ecosystem reach depends on whether the surrounding tools honor Standard Schema rather than only advertising generic validation support.

Use it if

  • Browser code needs runtime schemas whose unused validation functions can be removed by a modern tree-shaking bundler.
  • TypeScript should infer both the raw input and the transformed output from one executable definition.
  • Form or API validation needs issue paths, flattened field errors, defaults, transformations, and cross-field checks.
  • A framework accepts Standard Schema and can consume Valibot without a framework-specific adapter.
Skip it if

Setup reality

We installed Valibot 1.4.2 without a cache in a fresh Node 22 Bookworm container. npm completed in 1.1 seconds and left one package using 2 MB. Valibot declares zero direct dependencies and one peer dependency, with 1,824 KB unpacked and an MIT license. npm audit reported zero known vulnerabilities at critical, high, moderate, and low severity. Bundled TypeScript declarations were present.

The package is ESM with an exports map, yet both require() and ESM import worked on our Node 22 box. TypeScript 5 or newer is the declared peer. Valibot needs no credentials or config file. The documentation commonly uses import * as v from 'valibot'; static member access lets modern bundlers remove unused functions, while dynamic access such as v[actionName] prevents that analysis. Our deliberate full-namespace esbuild import measured 83.8 KB minified and 14.6 KB gzipped, so measure the schema imports that production actually ships.

Migrating from Zod changes the expression shape. Constraints and transforms become functions inside pipe(), and wrappers implement optional, nullable, fallback, or async behavior. Plain object() drops unknown keys. Use strictObject() when a misspelled config key should fail, looseObject() when extras must survive, or objectWithRest() when every extra value needs validation. Version 1.4.2 specifically fixes issue flattening and intersection merging for object keys that shadow names such as toString.

Async checks require async schema constructors plus parseAsync() or safeParseAsync(). TypeScript rejects common sync and async pairings, but plain JavaScript does not provide that compile-time guard, so test each asynchronous path at runtime. Transforms execute in pipe order, which means the schema should validate the value again after a coercion such as Number. Global message setters mutate shared module state; set them once during startup instead of swapping locales between concurrent requests.

Patterns

Parse unknown login data define-and-parse

import * as v from 'valibot';

const LoginSchema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
});

type LoginInput = v.InferInput<typeof LoginSchema>;   // before transforms
type LoginData = v.InferOutput<typeof LoginSchema>;   // after transforms

const data = v.parse(LoginSchema, await req.json());  // throws ValiError

if (v.is(LoginSchema, unknownValue)) {
  unknownValue.email;   // narrowed by the type guard
}

`InferInput` describes data before transformations, while `InferOutput` describes the successful result. Static namespace access can still tree-shake.

Turn validation issues into field errors handle-errors

import * as v from 'valibot';

const result = v.safeParse(LoginSchema, { email: '', password: 'x' });

if (!result.success) {
  v.flatten(result.issues);
  // { nested: { email: ['Invalid email: Received ""'],
  //             password: ['Invalid length: Expected >=8 but received 1'] } }

  result.issues.map((i) => v.getDotPath(i));   // ['email', 'password']

  console.error(v.summarize(result.issues));
  // x Invalid email: Received ""
  //   -> at email
} else {
  result.output;   // fully typed
}

Check `success` before reading `output`; `flatten()` suits form state and dot paths identify individual nested controls.

Distinguish optional and nullable values optional-and-defaults

import * as v from 'valibot';

const Settings = v.object({
  theme: v.optional(v.picklist(['light', 'dark']), 'light'),  // default
  bio: v.optional(v.string()),                                // may be undefined
  age: v.nullish(v.number()),                                 // null or undefined
  nickname: v.nullable(v.string()),                           // null allowed
  id: v.exactOptional(v.string()),                            // key absent, not undefined
});

v.parse(Settings, {});   // { theme: 'light', nickname: undefined ... }

A default can be a function for fresh values, and `exactOptional` treats an absent property differently from explicit `undefined`.

Pick an unknown-key policy object-strictness

import * as v from 'valibot';

v.parse(v.object({ a: v.string() }), { a: 'x', b: 1 });
// { a: 'x' }            unknown keys are dropped

v.safeParse(v.strictObject({ a: v.string() }), { a: 'x', b: 1 });
// issue: Invalid key: Expected never but received "b"

v.parse(v.looseObject({ a: v.string() }), { a: 'x', b: 1 });
// { a: 'x', b: 1 }      unknown keys kept as-is

v.parse(v.objectWithRest({ a: v.string() }, v.number()), { a: 'x', b: 2 });
// { a: 'x', b: 2 }      unknown keys validated against v.number()

Plain `object()` removes extras; `strictObject()` is usually the safer configuration schema because misspellings become issues.

Transform text and validate the result transform-and-coerce

import * as v from 'valibot';

// query string to number, then validate the number
const Page = v.pipe(v.unknown(), v.transform(Number), v.number(), v.minValue(1));
v.parse(Page, '42');   // 42

// string to Date
const Day = v.pipe(v.string(), v.isoDate(), v.transform((s) => new Date(s)), v.date());
v.parse(Day, '2026-08-06');   // Date

// never fail: substitute a value instead
v.parse(v.fallback(v.number(), 0), 'nope');   // 0

Actions run from left to right. After `Number`, include `number()` and range checks for the converted value.

Select a union member by discriminator unions-and-variants

import * as v from 'valibot';

const Event = v.variant('type', [
  v.object({ type: v.literal('click'), x: v.number(), y: v.number() }),
  v.object({ type: v.literal('key'), key: v.string() }),
]);

v.parse(Event, { type: 'key', key: 'a' });

v.safeParse(Event, { type: 'scroll' }).issues[0].message;
// 'Invalid type: Expected ("click" | "key") but received "scroll"'

// no shared discriminator: falls back to trying each in order
const Id = v.union([v.string(), v.pipe(v.number(), v.integer())]);

`variant()` uses the shared `type` field; a general union tries members in order and can return less focused issues.

Attach a cross-field issue to one control cross-field-validation

import * as v from 'valibot';

const Register = v.pipe(
  v.object({
    password: v.pipe(v.string(), v.minLength(8)),
    confirm: v.string(),
  }),
  v.forward(
    v.partialCheck(
      [['password'], ['confirm']],
      (input) => input.password === input.confirm,
      'Passwords do not match',
    ),
    ['confirm'],
  ),
);

const r = v.safeParse(Register, { password: 'aaaaaaaa', confirm: 'b' });
v.getDotPath(r.issues[0]);   // 'confirm'

`forward()` moves the password comparison issue onto `confirm`, where a form can display it beside the input.

Await a database-backed validation async-validation

import * as v from 'valibot';

const Signup = v.objectAsync({
  email: v.pipeAsync(
    v.string(),
    v.email(),
    v.checkAsync(async (e) => !(await db.userExists(e)), 'Email already taken'),
  ),
});

const result = await v.safeParseAsync(Signup, body);

// DANGER in plain JavaScript, a compile error in TypeScript:
v.parse(Signup, body);      // returns undefined
v.safeParse(Signup, body);  // { success: true }, no output, nothing rejected

Use async constructors and async parsers together. TypeScript catches the shown mismatch, but plain JavaScript may appear to succeed without output.

Alternatives

PackageRegistryPick it when
zodnpmUse it when chained methods, community examples, and integrations that explicitly require Zod outweigh client bundle concerns.
arktypenpmUse it when TypeScript-like schema expressions and its runtime checker fit the team's preferred authoring model.
superstructnpmUse it for a smaller established composable validator when Standard Schema and Valibot's transformations are unnecessary.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.