TypeScript and the runtime · 5 / 7
lesson 5

Schema validation alternatives

Valibot, ArkType, and the trade between bundle size, ergonomics, and what you already know.

~ 13 min read·lesson 5 of 7
0 / 7

Zod is the popular default and a fine one. It is also not the only library doing this job, and a few of the alternatives are worth knowing about — partly because they make different tradeoffs, partly because the choice affects bundle size in ways you'll notice on a marketing page that ships 40 KB of validators it barely uses. This lesson is a quick tour of the two most common alternatives and a frame for choosing.

Valibot

Valibot is the obvious answer to "I like Zod's idea but want a smaller bundle." It does the same job — runtime validation with TypeScript inference — and the API is recognizable.

valibot-user.ts
import * as v from "valibot";

const User = v.object({
id: v.string(),
name: v.string(),
age: v.number(),
});

const parsed = v.parse(User, raw);
type User = v.InferOutput<typeof User>;

The shape will feel familiar after Zod. The differences are mostly stylistic.

v.parse(schema, value) instead of schema.parse(value) — Valibot's API is functional, not method-chained. Schemas are plain values; the operations on them are top-level functions. This is what lets bundlers tree-shake the parts you don't use.

The inference type is v.InferOutput<typeof User> instead of z.infer. The name is more explicit — InferOutput because Valibot also has InferInput for cases where parsing transforms the value (think: a string is parsed into a Date). For schemas without transforms, the two are the same.

valibot-pipe.ts
const Email = v.pipe(
v.string(),
v.email("must be a valid email"),
v.maxLength(120),
);

Constraints are composed with v.pipe(schema, ...refinements). Where Zod chains methods (z.string().email().max(120)), Valibot lists each step. The pipe form is more verbose but it's also why Valibot tree-shakes: when you don't use email, the validator for it never enters the bundle. Zod's chained methods all live on the same object, so all of them ship whether you call them or not.

Tip

If your app uses validators in a few small places — a form here, a fetch wrapper there — Valibot can shave 10–20 KB compared to Zod. If you use validators everywhere, the gap shrinks because Zod's footprint is paid once.

check your understanding
What's the main reason Valibot bundles smaller than Zod for an app that only uses a few validators?

ArkType

ArkType is a different bet. It uses TypeScript-style strings as the schema language.

arktype-user.ts
import { type } from "arktype";

const User = type({
id: "string",
name: "string",
age: "number",
email: "string.email",
status: "'draft' | 'published' | 'archived'",
});

const result = User(raw);
if (result instanceof type.errors) {
console.error(result.summary);
} else {
/* result is the validated value */
}

The schema reads almost like a TypeScript type literal — except every value is a string the library parses. "string.email" is ArkType's way of saying "a string constrained to email format." The union syntax with single-quoted literals is the same shape you'd write in a TypeScript type.

The result of calling User(raw) is either the validated value or an instance of type.errors. The instanceof check splits the two paths — TypeScript narrows the variable to the validated shape on the success branch. It's a tagged result like Zod's safeParse, just with a different surface.

The pitch: if you're already fluent in TypeScript types, you can write schemas in syntax you already know. The downside: the schema is a string, which means typos don't get the same kind of editor feedback you'd get from a method call. ArkType's editor plugin and runtime errors do close most of that gap, but it's a different feel.

arktype-nested.ts
const Order = type({
id: "string",
items: {
  sku: "string",
  qty: "number > 0",
  price: "number >= 0",
} as const + "[]",
customer: {
  name: "string",
  email: "string.email | null",
},
});

Nesting works by composing object literals and adding [] for arrays. The constraints — "number > 0", "number >= 0" — are part of the string syntax. ArkType also produces faster runtime checks than Zod or Valibot for many shapes because it compiles the schema into a specialized validator function.

check your understanding
An ArkType schema uses "number > 0". What does that mean at runtime?

Reading the tradeoffs

Three libraries, three trade-offs. Putting them next to each other:

Zod Valibot ArkTypesize larger smallest small ergonomics chained piped strings runtime solid solid fastest community huge growing growing
Zod is the friendly default. Valibot trades a chained API for tree-shaking. ArkType trades familiar method calls for a string-based schema language and a faster runtime.

A library is more than its API. It's the docs you'll search, the GitHub issues you'll read at midnight, the StackOverflow answers, the Stack-shaped knowledge that gets baked into a community. Zod's lead there is real: most tutorials, most validators in the wild, most familiar idioms. The tradeoff: you're paying for that familiarity in bundle size.

A practical heuristic: a small, dynamic-imports-heavy app or a serverless function where cold-start size matters → Valibot is worth a real look. A large product with a Zod-shaped team, lots of existing schemas, and a budget for the extra KB → stick with Zod. A team that loves TypeScript-style strings or wants the fastest runtime for a hot validation path → ArkType.

Watch out

Don't mix two schema libraries in one app. The bundle cost adds up, and the engineers reading the code have to learn both APIs. Pick one and converge.

check your understanding
Your team ships a Next.js app with a marketing landing page (no validation needed) and a settings page (heavy validation). Bundle size on the landing page is a top metric. Which choice helps the most?

How to choose

The honest answer: pick Zod unless you have a reason. The reasons exist and they're real, but most apps don't have them.

Reasons to leave the default:

  • Bundle size matters and you're under a budget. Marketing pages, embeddable widgets, mobile-first SPAs that target slow networks. Valibot earns its keep here.
  • Validation is on a hot path. Game servers, real-time pipelines, edge functions billed per millisecond. ArkType's compiled validators measure faster than Zod's interpreted ones.
  • The team already loves TypeScript syntax for everything. ArkType's strings will feel natural; Zod's chained methods will feel like a translation.

Reasons to stay with Zod:

  • Most of what you read about validators on the web assumes Zod. The blog posts, the tutorials, the examples in framework docs. New teammates onboard faster.
  • The ecosystem. Zod adapters exist for tRPC, react-hook-form, sanity-zod, drizzle-zod, and a hundred others. Valibot has a growing list; ArkType less so.
  • You need consistency across many small validators. Zod's chained API is shorter for trivial schemas, and short matters when you have a hundred of them.

The losing strategy is to pick on vibes and switch a year later. The schemas don't migrate easily; consumers downstream end up rewriting parse calls and import statements across the codebase. Pick once, write the schemas centrally, and live with the call.

check your understanding
Your team is starting a new project. Half the engineers know Zod, half know nothing. You expect the validation surface to be small (one or two schemas per page, mostly forms). Which is the lowest-risk choice?
check your understanding
A teammate proposes using Zod on the server and Valibot on the client to "get the best of both." What's the problem?
check your understanding
An ArkType schema uses "'draft' | 'published' | 'archived'" for a status field. What is the inferred TypeScript type after parsing?
← prevnext lesson →
KeepLearningcertificate
for completing
TypeScript and the runtime
0 of 7 read