Integrate Valibot

This guide is aimed at library authors who want to build on top of Valibot — whether that is a form library, an ORM, an API framework, a code generator, or other tooling. It covers Standard Schema for schema-agnostic integrations, schema introspection for extracting types and runtime properties, and schema tree traversal for analysis and code generation.

Standard Schema

Valibot implements Standard Schema v1. Every schema object exposes a '~standard' property that provides a vendor-neutral validate function and inferred TypeScript types. We recommend reading the Standard Schema documentation for the full interface specification.

When building a library that accepts user-defined schemas, we recommend accepting a StandardSchemaV1 instead of a Valibot-specific type — unless your integration requires Valibot-specific APIs. This ensures your library works with any Standard Schema-compatible library, not just Valibot.

import type { StandardSchemaV1 } from '@standard-schema/spec';

async function validateData(schema: StandardSchemaV1, data: unknown) {
  const result = await schema['~standard'].validate(data);
  if (result.issues) {
    // Validation failed — result.issues is a readonly array of StandardIssue
    console.log(result.issues);
  } else {
    // Validation succeeded — result.value is the typed output
    console.log(result.value);
  }
}

One important limitation: '~standard'.validate always uses Valibot's global config. There is no way to pass a custom config (such as abortEarly or a custom lang) through the Standard Schema interface. If you need that level of control, use Valibot's own parsing APIs directly.

Valibot also supports the Standard JSON Schema specification via the @valibot/to-json-schema package, which exposes a toStandardJsonSchema function.

Schema introspection

Valibot schemas are plain objects, so all their properties are readable at runtime. This section covers how to extract static TypeScript types, read runtime properties, and use built-in type guards to narrow schema values safely.

Static types

Valibot exposes three generic utility types for extracting type information from any schema, validation, transformation, or metadata object.

import * as v from 'valibot';

const Schema = v.pipe(v.string(), v.decimal(), v.toNumber());

type Input = v.InferInput<typeof Schema>; // string
type Output = v.InferOutput<typeof Schema>; // number
type Issue = v.InferIssue<typeof Schema>; // StringIssue | DecimalIssue | ToNumberIssue

InferInput, InferOutput, and InferIssue read the phantom '~types' field. They work on schemas, validations, transformations, and metadata alike. '~types' is always undefined at runtime — this field exists solely for TypeScript's type inference, so we recommend never reading it in runtime code.

Runtime properties

Every schema and action is a plain object, so you can read its properties directly at runtime. The base properties (kind, type, async, etc.) are always present. Use kind to distinguish schemas from actions, and type to identify specific schemas and actions. Some schemas expose additional properties listed in the table below.

SchemaExtra propertyDescription
object, looseObject, strictObjectentriesRecord<string, BaseSchema> of named fields
objectWithRestentries, restnamed fields + rest element schema
arrayitemelement schema
tuple, looseTuple, strictTupleitemsordered tuple of element schemas
tupleWithRestitems, restordered elements + rest element schema
record, mapkey, valuekey and value schemas
setvaluevalue schema
union, intersectoptionsarray of member schemas
variantkey, optionsdiscriminant key string + array of object schemas
optional, nullable, and other wrapperswrappedinner schema
lazygetter(input: unknown) => BaseSchema deferred getter
any schema passed through pipepipetuple of the root schema followed by pipe items

Type guards

Use these helpers to narrow the TypeScript type of an unknown Valibot object before accessing its properties. Valibot exports three type guard helpers — isOfKind, isOfType, and isValiError — that narrow kind and type with TypeScript inference:

import * as v from 'valibot';

// Narrows to BaseSchema by kind
if (v.isOfKind('schema', item)) {
  item; // BaseSchema<...>
}

// Narrows to StringSchema by type
if (v.isOfType('string', schema)) {
  schema; // StringSchema<...>
}

Direct === comparisons on kind and type are fine too, but isOfKind and isOfType can better narrow the TypeScript type of the object in some edge cases.

isValiError is a separate helper for error handling. ValiError is the error class thrown by parse and parser. It extends Error with name = 'ValiError' and a typed issues array:

import * as v from 'valibot';

try {
  v.parse(Schema, input);
} catch (error) {
  if (v.isValiError<typeof Schema>(error)) {
    // error is ValiError<typeof Schema>
    console.log(error.issues);
  }
}

Schema tree traversal

Because schemas are plain objects, we can walk a schema tree by reading its properties (see Runtime properties). When traversing a piped schema, read the pipe tuple — its first item is the root schema and subsequent items are pipe actions or nested schemas.

Here is a simplified example inspired by getDefaults that extracts deeply nested default values from object and tuple schemas:

import * as v from 'valibot';

function getDefaults<
  const TSchema extends
    | v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>
    | v.ObjectSchema<v.ObjectEntries, v.ErrorMessage<v.ObjectIssue> | undefined>
    | v.TupleSchema<v.TupleItems, v.ErrorMessage<v.TupleIssue> | undefined>,
>(schema: TSchema): v.InferDefaults<TSchema> {
  // If it is an object schema, return defaults of entries
  if ('entries' in schema) {
    const object: Record<string, unknown> = {};
    for (const key in schema.entries) {
      object[key] = getDefaults(schema.entries[key]);
    }
    return object;
  }

  // If it is a tuple schema, return defaults of items
  if ('items' in schema) {
    return schema.items.map(getDefaults);
  }

  // Otherwise, return default or `undefined`
  return v.getDefault(schema);
}

Contributors

Thanks to all the contributors who helped make this page better!

  • GitHub profile picture of @fabian-hiller

Partners

Thanks to our partners who support the project ideally and financially.

Sponsors

Thanks to our GitHub sponsors who support the project financially.

  • GitHub profile picture of @antfu
  • GitHub profile picture of @UpwayShop
  • GitHub profile picture of @vasilii-kovalev
  • GitHub profile picture of @saturnonearth
  • GitHub profile picture of @ruiaraujo012
  • GitHub profile picture of @hyunbinseo
  • GitHub profile picture of @nickytonline
  • GitHub profile picture of @KubaJastrz
  • GitHub profile picture of @kibertoad
  • GitHub profile picture of @Thanaen
  • GitHub profile picture of @caegdeveloper
  • GitHub profile picture of @bmoyroud
  • GitHub profile picture of @dslatkin