Free & open source — no account required

v1.2.5-PRICING-19
Web & Frontend • Engineering Documentation

Valibot Mastery: Minimalist Schema Generation

This technical guide provides an in-depth analysis of the json to valibot engine, best practices for implementation, and data security standards.

JSON to Valibot: Tree-Shakeable Validation with Per-Feature Bundle Costs

Valibot is built on a single observation: most validation libraries are monolithic — you ship the whole library even if you only use 10% of it. Valibot inverts this by defining every validator as an independently importable function. Your bundle includes only the validators you actually call. For a Next.js app with a small checkout form, that can mean 0.6kb of validation code instead of 12kb. Converting your JSON to Valibot schemas is a direct investment in client-side performance — the JSON shape you paste in determines exactly which validator functions get imported.

Live Example: Order Schema with Pipeline Validations

// Input JSON
{
  "orderId": "ORD-123",
  "customer": "Jane Doe",
  "amount": 150.50,
  "currency": "USD",
  "items": [
    { "sku": "WGT-001", "qty": 2, "unitPrice": 75.25 }
  ]
}

// Generated Valibot Schema
import * as v from 'valibot';

const OrderItemSchema = v.object({
  sku:       v.pipe(v.string(), v.regex(/^[A-Z]{3}-\d{3}$/)),
  qty:       v.pipe(v.number(), v.integer(), v.minValue(1)),
  unitPrice: v.pipe(v.number(), v.minValue(0)),
});

const OrderSchema = v.object({
  orderId:  v.pipe(v.string(), v.regex(/^ORD-\d+$/)),
  customer: v.pipe(v.string(), v.minLength(2), v.maxLength(100)),
  amount:   v.pipe(v.number(), v.minValue(0)),
  currency: v.picklist(['USD', 'EUR', 'GBP', 'JPY']),
  items:    v.pipe(v.array(OrderItemSchema), v.minLength(1)),
});

type Order = v.InferOutput<typeof OrderSchema>;

const result = v.safeParse(OrderSchema, incomingData);
if (!result.success) {
  const flat = v.flatten<typeof OrderSchema>(result.issues);
  // flat.nested['items.0.qty'] — path-keyed errors
  console.error(flat.nested);
}

The v.pipe() Architecture: Validators as Data

Valibot separates schemas (what type is this?) from actions (what constraints does it have?). This is the architectural reason for its small bundle:

// Without pipe — schema only, no constraints
// Bundle cost: just v.string()
const NameSchema = v.string();

// With pipe — adds only the actions you use
// Bundle cost: v.string() + v.minLength + v.maxLength + v.trim
const NameSchema = v.pipe(
  v.string(),
  v.trim(),           // strip whitespace before validation
  v.minLength(1, 'Name is required'),
  v.maxLength(50, 'Name too long')
);

// Compare with Zod:
// z.string().trim().min(1).max(50)
// Ships the entire z.string() prototype chain regardless

// Valibot ships only what is imported:
import { string, pipe, trim, minLength, maxLength } from 'valibot';
// Only these 5 functions in your bundle

A bundler using tree-shaking can statically analyze these imports and eliminate every Valibot function you don't import. With Zod's chained method API, the entire prototype chain must be included because the bundler can't prove which methods will be called at runtime.

Transforms and Custom Actions in Pipelines

Valibot's v.transform() and v.check() integrate directly into the validation pipeline — validate and transform in a single pass:

const UserSchema = v.object({
  // Coerce ISO string to Date, validate range
  createdAt: v.pipe(
    v.string(),
    v.isoTimestamp(),
    v.transform(s => new Date(s)),
    v.check(d => d <= new Date(), 'Date cannot be in the future'),
  ),

  // Normalize email to lowercase before storage
  email: v.pipe(
    v.string(),
    v.email(),
    v.transform(s => s.toLowerCase()),
  ),

  // Custom business rule: admin requires email domain
  role: v.picklist(['admin', 'user', 'guest']),
});

// Cross-field validation with v.check() at the object level
const AdminUserSchema = v.pipe(
  UserSchema,
  v.check(
    user => user.role !== 'admin' || user.email.endsWith('@company.com'),
    'Admin accounts must use a company email address'
  )
);

Discriminated Unions for Polymorphic JSON

const ClickEventSchema = v.object({
  type: v.literal('click'),
  x: v.number(),
  y: v.number(),
});

const KeyEventSchema = v.object({
  type: v.literal('keypress'),
  key: v.string(),
  ctrlKey: v.boolean(),
});

const DomEventSchema = v.variant('type', [
  ClickEventSchema,
  KeyEventSchema,
]);

type DomEvent = v.InferOutput<typeof DomEventSchema>;

// v.variant() is Valibot's equivalent of z.discriminatedUnion()
// It checks the discriminator key first — O(1) lookup

Integration with SvelteKit Form Actions

Valibot's small size and modular design make it ideal for form validation in frameworks that run on the edge (Cloudflare Workers, Vercel Edge):

// src/routes/checkout/+page.server.ts (SvelteKit)
import * as v from 'valibot';
import { fail } from '@sveltejs/kit';

const CheckoutSchema = v.object({
  email:    v.pipe(v.string(), v.email()),
  name:     v.pipe(v.string(), v.minLength(2)),
  cardLast4: v.pipe(v.string(), v.regex(/^\d{4}$/)),
});

export const actions = {
  default: async ({ request }) => {
    const formData = Object.fromEntries(await request.formData());
    const result = v.safeParse(CheckoutSchema, formData);

    if (!result.success) {
      return fail(400, {
        errors: v.flatten(result.issues).nested,
        values: formData,
      });
    }

    // result.output is typed CheckoutOutput
    await processCheckout(result.output);
  }
};

At Cloudflare Workers' 1MB worker size limit, Valibot's sub-1kb footprint for simple schemas leaves room for actual business logic. Zod's 12kb baseline consumes 1.2% of the total limit before any application code.

v.flatten() Error Format for Forms

const result = v.safeParse(OrderSchema, rawInput);

if (!result.success) {
  const flat = v.flatten<typeof OrderSchema>(result.issues);

  // flat.root — top-level errors (e.g., wrong type for the whole input)
  // flat.nested — field-keyed errors
  // {
  //   "orderId":      ["Invalid format"],
  //   "items.0.qty":  ["Must be at least 1"],
  //   "items.0.sku":  ["Does not match pattern"]
  // }

  // Map to form field error display
  Object.entries(flat.nested ?? {}).forEach(([path, messages]) => {
    console.log(`${path}: ${messages?.[0]}`);
  });
}

The nested path format (items.0.qty) maps directly to dot-notation accessors used by form libraries like React Hook Form, Superforms (SvelteKit), or FormKit (Vue).

Lazy Schemas for Recursive JSON Structures

// Recursive type: tree node with children
type TreeNode = {
  id: string;
  label: string;
  children: TreeNode[];
};

// v.lazy() defers schema resolution — required for recursive types
const TreeNodeSchema: v.GenericSchema<TreeNode> = v.object({
  id:       v.string(),
  label:    v.string(),
  children: v.lazy(() => v.array(TreeNodeSchema)),
});

Best Practices for Production

  • Always use v.safeParse() for external data: v.parse() throws on failure — fine for startup config, not for user input or API responses. v.safeParse() returns a discriminated union you handle explicitly.
  • Define schemas at module level: Don't define schemas inside request handlers or React components. Module-level definitions are parsed once; handler-level definitions re-parse on every call.
  • Use v.InferInput for form states and v.InferOutput for domain models: Input is the raw type before transforms (string dates, uncoerced numbers); output is the transformed type your app logic works with.
  • Measure actual bundle impact: Run npx bundle-wizard or use Rollup's bundle analyzer to confirm tree-shaking is working. Some bundler configs disable tree-shaking for node_modules — verify with sideEffects: false in the Valibot package.json (it's already set).

FAQ

Q: How much smaller is Valibot than Zod in practice?
A: For a simple schema validating 5 string fields with email/URL/min checks, Valibot typically ships ~600 bytes gzipped versus Zod's ~11kb gzipped. For schemas using most of Zod's feature set (transforms, discriminated unions, refinements), the gap narrows — roughly 2-3kb vs 12kb. The savings are most significant for client-side validation with constrained schemas.

Q: Does Valibot work with React Hook Form?
A: Yes. Use @hookform/resolvers/valibot: resolver: valibotResolver(YourSchema). The error format from v.flatten() maps correctly to RHF's field error structure.

Q: Can I use Valibot on the server as well as the client?
A: Yes — Valibot works in Node.js, Bun, Deno, Cloudflare Workers, and any browser environment. The bundle size advantage matters most for client-side code; on the server, use whichever library has the best DX for your team.

Q: What is the difference between v.InferInput and v.InferOutput?
A: InferInput is the raw type before transforms — what you pass to safeParse(). InferOutput is the type after transforms — what you get back from a successful parse. If your schema has a v.transform(s => new Date(s)) on a date field, InferInput has string and InferOutput has Date.

Q: Is Valibot stable enough for production?
A: Valibot v1.0 is stable. It is used in production by teams in the SvelteKit, Remix, and Nuxt ecosystems. The API has been stable since the v1 release, with no breaking changes expected.

Developer FAQ

Is the processing local-only?

Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.

Can I use this for enterprise projects?

Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.