npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

valibot-serialize

v2.0.1

Published

Generate deterministic, tree-shakeable Valibot modules and serialize schemas as a portable AST

Downloads

488

Readme

valibot-serialize

Generate deterministic, tree-shakeable Valibot modules from Valibot schemas and optional Drizzle tables, through a CLI or non-exiting programmatic API.

The generator is built on a stable, portable serialized AST that is also available directly for schema storage, migration, reconstruction, and code generation.

Copyright (c) 2025 by Gadi Cohen. MIT licensed.

npm Tests coverage semantic-release TypeScript MIT License

Install and runtimes

The npm package supports Node.js 22 and later. Install tsx, its required CLI runtime peer, when using the packaged vs_tocode binary:

npm add valibot valibot-serialize
npm add --save-dev tsx

For Deno, use the JSR package and Valibot:

deno add jsr:@gadicc/valibot-serialize npm:valibot

AI agent skill

This repository includes a valibot-serialize skill that gives compatible AI coding agents package-aware generation, serialization, migration, conversion, and source-plugin guidance. Add it to a project with the skills CLI:

npx skills add gadicc/valibot-serialize --skill valibot-serialize

The interactive command detects supported agents and lets you choose the installation scope. To install it globally for Codex without prompts, run:

npx skills add gadicc/valibot-serialize --skill valibot-serialize --agent codex --global --yes

Generate schemas

The generator scans selected modules for supported exports and writes static Valibot modules. A typical project keeps generation and CI verification next to each other in package.json:

{
  "scripts": {
    "schema:gen": "vs_tocode --include 'src/db/schema/*.ts' --out-dir src/db/valibot/generated --formatter=none",
    "schema:check": "vs_tocode --include 'src/db/schema/*.ts' --out-dir src/db/valibot/generated --formatter=none --check"
  }
}

Run npm run schema:gen to write the modules. Run npm run schema:check in CI to compare the exact generated text without writing; it exits nonzero when a selected source's output is missing or different. Check mode covers selected inputs only and does not find orphan outputs whose source is no longer selected.

Generated files can be committed for reviewable diffs or produced as a build step. Whichever policy you choose, keep it consistent and run schema:check when committed output must stay synchronized. Output is deterministic for identical sources, options, plugin and dependency versions, and an explicit formatter choice. Select a pinned formatter or none in CI instead of the environment-sensitive auto mode.

Built-in sources and optional integrations

The built-in handlers run in stable order:

  1. Drizzle tables, when both drizzle-orm and drizzle-valibot are installed.
  2. Valibot schemas.

Drizzle and formatter integrations are optional; core serialization and Valibot-schema generation do not require them. Formatting is powered by projectfmt, which uses project-local Prettier or Biome configuration (and dependencies), or Deno fmt, when explicitly selected or detected by auto.

For example, this Drizzle input:

import { integer, pgTable, text } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: integer().generatedAlwaysAsIdentity().primaryKey(),
  name: text().notNull(),
});

produces tree-shakeable Valibot exports shaped like:

export const usersSelect = v.object({
  /* generated fields */
});
export const usersInsert = v.object({
  /* generated insert fields */
});
export const usersUpdate = v.object({
  /* generated optional update fields */
});
export type UsersSelect = v.InferOutput<typeof usersSelect>;
export type UsersInsert = v.InferInput<typeof usersInsert>;
export type UsersUpdate = v.InferInput<typeof usersUpdate>;

If you need a different Drizzle-derived shape, use drizzle-valibot directly, export the Valibot schemas you want, and let the Valibot handler generate them.

Watch workflow

Pass --watch to keep selected outputs converged as sources change:

npm run schema:gen -- --watch

Watch mode owns only outputs mapped from the selected inputs. It serializes event handling, rejects output collisions, removes only outputs it owns, and keeps running after a per-file generation error so a later edit can recover.

For one-off use, the same binary can be run directly:

npx -p valibot-serialize vs_tocode --help
deno run --allow-read --allow-write jsr:@gadicc/valibot-serialize/vs_tocode --help

Programmatic generation

Import the non-exiting API from the dedicated subpath. Errors reject the returned promise; the API never terminates its host process or sets an exit status. A caller that enables watch mode owns the returned lifecycle:

import { generate } from "valibot-serialize/vs_tocode";

const result = await generate({
  explicitFiles: ["src/db/schema/users.ts"],
  // Or select files with: include: ["src/db/schema/*.ts"],
  outDir: "src/db/valibot/generated",
  formatter: "none",
  watch: true,
});

console.log(`Generated ${result.files.length} file(s)`);

// Close during application shutdown. close() is safe to call more than once.
await result.watch?.close();
await result.watch?.done;

Programmatic check mode returns structured, canonically ordered mismatches and does not write or remove files:

const result = await generate({
  explicitFiles: ["src/db/schema/users.ts"],
  outDir: "src/db/valibot/generated",
  formatter: "none",
  check: true,
});

if (!result.check?.upToDate) {
  console.error(result.check?.mismatches);
}

Relative inputs, globs, and output paths resolve from projectRoot, which defaults to the current working directory. Check mode cannot be combined with watch or dryRun.

Explicit source plugins

Third-party sources are explicit: pass handlers to replace the built-ins, and include builtInHandlers yourself when composing custom and built-in behavior. Supplied order is preserved and every synchronous or asynchronous hook is awaited in that order.

import {
  builtInHandlers,
  defineSourcePlugin,
  generate,
} from "valibot-serialize/vs_tocode";

const taggedStringPlugin = defineSourcePlugin({
  name: "tagged-string",
  available: () => true,
  test: (value) =>
    typeof value === "object" && value !== null &&
    (value as { kind?: unknown }).kind === "tagged-string",
  transform: (symbol) => ({ exports: { [symbol]: "v.string()" } }),
});

await generate({
  explicitFiles: ["src/schemas.ts"],
  outDir: "src/generated",
  formatter: "none",
  handlers: [taggedStringPlugin, ...builtInHandlers],
});

See the source plugin guide for stable types, contexts, failure behavior, and composition rules. Plugins are never discovered implicitly.

Serialize schemas

The serialized AST is a versioned JSON wire format and the foundation of the generator. It is also a public API for applications that need to store, transport, inspect, migrate, or reconstruct schemas.

import * as v from "valibot";
import * as vs from "valibot-serialize";

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

const serialized = vs.fromValibot(LoginSchema);
const wireText = JSON.stringify(serialized);
const received: unknown = JSON.parse(wireText);
const migrated = vs.migrateSerializedSchema(received);
const NewLoginSchema = vs.toValibot(migrated);

const parsed = v.parse(NewLoginSchema, {
  email: "[email protected]",
  password: "password",
});

const code = vs.toCode(migrated);
// "v.object({email:v.string(),password:v.string()})"

Serialization API

  • fromValibot(schema: v.BaseSchema): SerializedSchema
    • Encodes a Valibot schema to a JSON‑serializable AST with { kind, vendor, version, format, node }.
  • toValibot(data: SupportedSerializedSchema): v.BaseSchema
    • Decodes current format 2 or supported legacy format 1 back to a Valibot schema.
  • isSerializedSchema(x: unknown): x is SerializedSchema
    • Runtime type guard for the current format-2 AST envelope only.
  • migrateSerializedSchema(x: unknown): SerializedSchema
    • Validates format 1 or 2 and returns a detached canonical format-2 payload.
  • SerializedSchemaV1 and SupportedSerializedSchema
    • Exact legacy input and current-or-legacy reader types.
  • serializedSchemaJson
    • JSON Schema for the AST envelope and node variants (useful to validate serialized payloads).
  • ENVELOPE_VERSION and FORMAT_VERSION
    • Current routing-envelope and serialized-AST versions for public format introspection.
  • toJsonSchema(serialized: SupportedSerializedSchema): JsonSchema
    • Best‑effort conversion from our AST to JSON Schema (Draft 2020‑12) for data validation.
  • fromJsonSchema(json: JsonSchemaLike): SerializedSchema
    • Basic, lossy converter from a subset of JSON Schema → our AST (strings/numbers/booleans/literals/arrays/objects/enums/unions/tuples/sets/maps approximations).
  • toCode(serialized: SupportedSerializedSchema): string
    • Emits concise Valibot builder code for the given AST (no imports). Intended for code‑gen/export; format it as you like.

Format support

The serialized-format compatibility policy defines the envelope and AST version boundaries. In summary:

| Surface | Format support | | ----------------------------------------------- | ------------------------------------------- | | fromValibot writer and FORMAT_VERSION | Emits canonical format 2 | | isSerializedSchema and serializedSchemaJson | Validate canonical format 2 | | migrateSerializedSchema | Reads format 1 or 2 and returns format 2 | | toValibot, toCode, and toJsonSchema | Read supported format 1 or current format 2 |

Format 2 represents repeated or recursive schema identities with canonical JSON Pointers such as { type: "reference", path: "#/node" }. Format-1 libraries cannot read format-2 output.

Limitations and non-goals

  • Arbitrary transforms, callbacks, and accessors cannot be serialized. Apply custom behavior outside the serialized schema. Unsupported pipe actions fail during fromValibot instead of disappearing.
  • Recursive lazy schemas round-trip through JSON, toValibot, and toCode. Recursive data JSON Schema conversion remains unsupported and throws a controlled cyclic-schema error.
  • Wrapper defaults round-trip only when they are exact JSON values: null, strings, booleans, finite numbers other than negative zero, dense arrays, and plain objects containing the same values. Callback defaults and richer runtime values fail without invoking callbacks or accessors. Default containers are limited to 256 levels and 10,000 visited containers. Proxies are unsupported.
  • fromJsonSchema is intentionally minimal and lossy. Prefer Valibot plus fromValibot as the source of truth.
  • Source plugins are explicit and local to each generate call. There is no implicit discovery, generalized target-plugin system, or automatic orphan cleanup.

Detailed reference

Module structure

  • Each Valibot schema kind is implemented in its own module under src/types/. For example: string.ts, number.ts, object.ts, enum.ts, and picklist.ts. This keeps detection/encode/decode/codegen/JSON‑Schema logic focused and easy to maintain. When adding support for a new schema, prefer creating src/types/<kind>.ts and export it via src/types/index.ts.

Supported nodes and flags (AST)

  • string with:
    • lengths: minLength, maxLength, exact length
    • patterns: pattern (+ patternFlags), startsWith, endsWith
    • formats/validators: email, rfcEmail, url, uuid, ip, ipv4, ipv6, hexColor, slug, digits, emoji, hexadecimal, creditCard, imei, mac, mac48, mac64, base64, ids ulid, nanoid, cuid2, ISO time/date variants isoDate, isoDateTime, isoTime, isoTimeSecond, isoTimestamp, isoWeek
    • counters: minGraphemes, maxGraphemes, minWords, maxWords; word counters retain their optional locale string or string array in minWordsLocales and maxWordsLocales
    • transforms: trim, trimStart, trimEnd, toUpperCase, toLowerCase, normalize; explicit Unicode forms use { type: "normalize", form: "NFC" | "NFD" | "NFKC" | "NFKD" }
  • number with min, max, gt, lt, integer, safeInteger, multipleOf, finite
  • boolean, literal
  • array with item + minLength, maxLength, length
  • object with entries, optionalKeys hint, policy (loose/strict), rest, minEntries, maxEntries
  • optional, nullable, nullish, exactOptional, and undefinedable, with exact JSON-value defaults
  • union, tuple (+ rest), record
  • enum with values
  • picklist with values (string options)
  • set with value, minSize, maxSize
  • map with key, value, minSize, maxSize
  • date, file (minSize, maxSize, mimeTypes), blob (minSize, maxSize, mimeTypes)

JSON Schema conversion

This was never a main goal for the project especially since other, mature tools exist for this purpose (i.e. @valibot/to-json-schema and json-schema-to-valibot, however, the AI offered to implement it and I said why not :) Let us know if you find it useful.

  • toJsonSchema converts:
    • Strings to string schemas, mapping common formats and adding regexes for selected validators (see notes).
      • IDs approximated: ulid, nanoid, cuid2 via patterns.
      • Validators approximated: creditCard, imei, mac, mac48, mac64, base64 via patterns.
    • Numbers, booleans, arrays, objects, tuples, enums, unions, sets/maps (approximate), records (as additionalProperties), date/file/blob as strings (binary for file/blob).
    • Union of literals becomes an enum.
  • fromJsonSchema converts back a subset:
    • type string/number/integer/boolean, const (literal), enum, array/object, tuple (prefixItems), union (anyOf), and anyOf of constants → picklist (all strings) or enum (mixed types).
    • Recognizes string format/email/uri/uuid/ipv4/ipv6, and common patterns produced by toJsonSchema for startsWith/endsWith, hexColor, slug, digits, hexadecimal, ids (ulid, nanoid, cuid2) and sets flags accordingly.

Compatibility mapping (selected)

| Valibot/AST | toJsonSchema | fromJsonSchema back | | -------------------------- | ------------------------------------- | ------------------- | | string.email | type: string, format: email | email: true | | string.url | type: string, format: uri | url: true | | string.uuid | type: string, format: uuid | uuid: true | | string.ipv4/ipv6 | format: ipv4/ipv6 | ipv4/ipv6: true | | string.ip | anyOf [ipv4, ipv6] | ip: true | | string.startsWith/endsWith | pattern/allOf anchored | starts/ends: true | | string.hexColor | regex | hexColor: true | | string.slug | regex | slug: true | | string.digits/hexadecimal | regex | digits/hexadecimal | | ulid/nanoid/cuid2 | regex | flags: true | | creditCard/imei/mac/... | regex | flags: true | | number min/max/gt/lt | min/max/exclusiveMin/Max | fields restored | | array min/max/len | minItems/maxItems | fields restored | | object min/max entries | minProperties/maxProperties | fields restored | | union of literals | enum | enum node | | enum values | enum | enum node | | set/map | array uniqueItems / object additional | approximated | | tuple/rest | prefixItems (+ items/rest) | fields restored | | date | string (format: date-time) | approximated | | file/blob | string binary (+ mediaType) | approximated |

Creation Notes

This was "vibe-coded" (with AI) over a weekend. I set up minimalist structure with a test case for how I wanted the code to work, and some empty functions with signatures. I then asked OpenAI Codex to complete the code.

Codex did so, and consistently gave some great suggestions on what to do next, and I kept saying yes to see where it would go. Eventually then I moved on to prompts for cleanup, refactoring, project structure, etc. The CLI tool was written by hand.

Please do bring any weird issues to our attention, and feel free to request clearer docs, examples, etc. Working on that next.

Relevant issues / discussions on valibot repo:

Development

See CONTRIBUTING.md for project layout, test naming, and workflow conventions.

License

MIT