{"id":174,"date":"2026-05-01T05:04:12","date_gmt":"2026-05-01T05:04:12","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/valibot-vs-zod-2026-i-half-migrated-and-stopped\/"},"modified":"2026-05-01T05:04:12","modified_gmt":"2026-05-01T05:04:12","slug":"valibot-vs-zod-2026-i-half-migrated-and-stopped","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/valibot-vs-zod-2026-i-half-migrated-and-stopped\/","title":{"rendered":"Valibot vs Zod in 2026: I Half-Migrated and Stopped"},"content":{"rendered":"<p>Confession: I&rsquo;ve been typing <code>z.object({...})<\/code> so reflexively for the past five years that when a teammate suggested swapping to Valibot, my first reaction was a tired &ldquo;why would I do that to myself.&rdquo; Then I actually looked at the bundle.<\/p>\n<p>Our app&rsquo;s client-side validation pulled in 13.4 KB of gzipped Zod just to parse three forms. That&rsquo;s not a crisis, but it&rsquo;s not nothing on a low-end Android phone. Valibot&rsquo;s pitch is simple: same idea, tree-shakable, often a fraction of the size. The marketing pages will tell you it&rsquo;s &ldquo;57x smaller.&rdquo; The marketing pages are lying in a normal marketing way, but the real number for a typical schema is still good enough that I took it seriously.<\/p>\n<p>So I migrated. Sort of. Then I stopped. This post is what I learned about both libraries, what made me switch on the client, and what kept Zod on the server. If you&rsquo;re staring at the same choice, here&rsquo;s the honest middle answer.<\/p>\n<h2 id=\"why-i-even-looked-at-valibot\">Why I even looked at Valibot<\/h2>\n<p>The pitch comes down to bundle size. The &ldquo;functional pipeable API&rdquo; framing is real but it&rsquo;s a taste preference, roughly as religious as tabs vs spaces. Tree-shaking is table stakes on any modern bundler.<\/p>\n<p>For a real comparison, I built the same schema in both libraries: a sign-up form with email, password rules, optional referral code, and a nested address. With Zod, the import cost about 13.4 KB gzipped. With Valibot&rsquo;s <code>parse<\/code>, <code>object<\/code>, <code>string<\/code>, <code>email<\/code>, <code>minLength<\/code>, <code>optional<\/code>, and the rest of what I needed, it came out around 1.1 KB. That&rsquo;s not 57x. It&rsquo;s about 12x. And 12x is still very good.<\/p>\n<p>The catch: Valibot&rsquo;s bundle wins because it&rsquo;s modular by design. You import every validator individually. That feels weird at first if you&rsquo;re coming from <code>z.string().email().min(8)<\/code>.<\/p>\n<pre><code class=\"language-ts\">\/\/ Zod\nconst Signup = z.object({\n  email: z.string().email(),\n  password: z.string().min(8),\n});\n\n\/\/ Valibot\nimport { object, string, email, minLength, parse, pipe } from &quot;valibot&quot;;\nconst Signup = object({\n  email: pipe(string(), email()),\n  password: pipe(string(), minLength(8)),\n});\n<\/code><\/pre>\n<p>The Valibot version is a few more lines. In return, your bundler can drop anything you don&rsquo;t actually use. Worth it on the client. Mostly noise on the server. See <a href=\"https:\/\/valibot.dev\/\" rel=\"nofollow noopener\" target=\"_blank\">Valibot&rsquo;s docs<\/a> for the full API surface.<\/p>\n<h2 id=\"the-api-differences-that-actually-matter\">The API differences that actually matter<\/h2>\n<p>Bundle size is the headline. Day-to-day, the API differences matter more.<\/p>\n<p>Zod uses method chaining. Every transform, refine, or default lives on the schema object. Valibot uses standalone functions composed via <code>pipe<\/code>. After about a week, both feel fine. The honest difference is that <code>pipe<\/code> plays better with TypeScript inference in some weird edge cases, particularly when you&rsquo;re chaining custom validators. Zod&rsquo;s chain is more discoverable in editor autocomplete because everything hangs off <code>z.<\/code>.<\/p>\n<p>A real example. Validating a string that should be a UUID, lowercased:<\/p>\n<pre><code class=\"language-ts\">\/\/ Zod 3.x\nconst Id = z.string().uuid().transform((s) =&gt; s.toLowerCase());\n\n\/\/ Valibot\nconst Id = pipe(string(), uuid(), transform((s) =&gt; s.toLowerCase()));\n<\/code><\/pre>\n<p>Same length. Slightly different mental model. The difference isn&rsquo;t ergonomic. It&rsquo;s where the type errors point. Zod errors cluster on <code>.uuid()<\/code> if you typo something. Valibot errors point at <code>pipe<\/code>. After two months of writing both, I had a clear preference: Zod for code I&rsquo;m reading on a screen share, Valibot for code I&rsquo;m tree-shaking.<\/p>\n<p>For error formatting, both libraries return a structured error tree. <a href=\"https:\/\/zod.dev\/\" rel=\"nofollow noopener\" target=\"_blank\">Zod&rsquo;s <code>error.format()<\/code><\/a> is more polished out of the box. Valibot&rsquo;s <code>flatten()<\/code> is leaner but you&rsquo;ll write your own formatter if your UI cares about per-field nested error paths. I wrote one in about 30 lines. It wasn&rsquo;t a tax I expected.<\/p>\n<h2 id=\"where-zod-still-wins\">Where Zod still wins<\/h2>\n<p>I went in expecting &ldquo;Zod wins on ecosystem&rdquo; to be the headline. It is, but not for the reason I thought.<\/p>\n<p>The real win isn&rsquo;t the number of integrations. It&rsquo;s the second-order stuff. <code>drizzle-zod<\/code> gives you schemas auto-generated from your DB tables. tRPC defaults to Zod. Hono&rsquo;s validator middleware ships with Zod adapters by default. Most OpenAPI generators assume Zod. Every one of these has a Valibot equivalent in some state of completeness, and &ldquo;some state of completeness&rdquo; is doing a lot of work in that sentence.<\/p>\n<p>If you&rsquo;re picking between them on a brand new project, this matters less than it sounds. If you&rsquo;re inside an existing app, it matters a lot. I&rsquo;ve been migrating an app from Prisma to Drizzle, and <code>drizzle-zod<\/code> lets me skip writing schemas by hand. There&rsquo;s a <code>drizzle-valibot<\/code> package, and it works, but it lags behind on edge cases. I hit one bug with nullable JSONB columns in March that was still open the last time I checked.<\/p>\n<p>Server-side, this is the whole game. I have schemas being shared between database, API, and client validation. Switching one to Valibot meant either translating at the boundary or migrating everything. Neither was a good Tuesday.<\/p>\n<p>So the rule I landed on: client-bundled schemas go to Valibot. Server-side schemas, especially anything shared with the database, stay on Zod. Boring, but it works.<\/p>\n<p>I covered a related case in <a href=\"https:\/\/abrarqasim.com\/blog\/typescript-branded-types-when-i-actually-use-them\" rel=\"noopener\">my post on TypeScript branded types<\/a>, where the same &ldquo;runtime validation meets the type system&rdquo; tension shows up.<\/p>\n<h2 id=\"my-partial-migration-what-i-moved-what-i-didnt\">My partial migration: what I moved, what I didn&rsquo;t<\/h2>\n<p>Concrete numbers from a small SaaS app. Before:<\/p>\n<ul>\n<li>Sign-up form on Zod, client-bundled.<\/li>\n<li>Profile edit form on Zod, client-bundled.<\/li>\n<li>API route validators on Zod, server only.<\/li>\n<li>Drizzle inferred schemas, server only.<\/li>\n<\/ul>\n<p>What I moved:<\/p>\n<ul>\n<li>Sign-up form to Valibot. Saved about 11 KB gzipped from the auth bundle.<\/li>\n<li>Profile edit form to Valibot. Saved another 8 KB.<\/li>\n<\/ul>\n<p>What I left:<\/p>\n<ul>\n<li>API route validators stayed on Zod. Not in the client bundle, so size doesn&rsquo;t matter.<\/li>\n<li>Drizzle schemas stayed on Zod because <code>drizzle-valibot<\/code> isn&rsquo;t there yet.<\/li>\n<\/ul>\n<p>Net result: the auth-and-onboarding bundle dropped about 18 KB gzipped. Real LCP improvement on a slow phone in our RUM data: about 140 ms. Not life-changing. Worth a Tuesday.<\/p>\n<p>The migration itself took about four hours including tests, mostly because I was relearning the API. The second form took 30 minutes.<\/p>\n<h2 id=\"what-about-arktype\">What about ArkType?<\/h2>\n<p>ArkType is the obvious next question. It tries something genuinely clever: parse TypeScript-flavored type strings at runtime.<\/p>\n<pre><code class=\"language-ts\">import { type } from &quot;arktype&quot;;\nconst User = type({\n  email: &quot;string&quot;,\n  age: &quot;number &gt; 0&quot;,\n});\n<\/code><\/pre>\n<p>Looks great in slides. In practice, the editor support is meaningfully worse than either Zod or Valibot. You&rsquo;re writing your schema as a string, so your editor&rsquo;s TypeScript service doesn&rsquo;t help you. The plugin work is real but uneven. Bundle size is in the same ballpark as Valibot. Error messages are arguably better than both.<\/p>\n<p>I&rsquo;d watch <a href=\"https:\/\/arktype.io\/\" rel=\"nofollow noopener\" target=\"_blank\">ArkType<\/a>. I wouldn&rsquo;t bet a 2026 codebase on it yet. If you&rsquo;re picking today, it&rsquo;s still a Zod-or-Valibot choice for most teams.<\/p>\n<p>Always check the bundle yourself with <code>pnpm dlx source-map-explorer<\/code> before you decide. The README benchmarks across all three projects are not lying, but they&rsquo;re also not your app.<\/p>\n<h2 id=\"what-to-actually-try-this-week\">What to actually try this week<\/h2>\n<p>Pick one client-bundled schema in your app. A form validator is the obvious one. Migrate just that schema to Valibot. Run the bundle analyzer before and after. If the savings are over 5 KB gzipped, you have your answer for that part of the codebase. If they&rsquo;re under 2 KB, stay on Zod and stop reading internet posts about validation libraries (including this one).<\/p>\n<p>The annoying truth is that this isn&rsquo;t a religious choice. It&rsquo;s a per-bundle calculation. I run both libraries in the same repo and the only person who&rsquo;s noticed is me, on the rare days I have to remember which file uses which API.<\/p>\n<p>If you&rsquo;re more interested in the bundle-size side of life, I write about practical performance and tooling work in <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">my recent project work<\/a>. If you do migrate a form this week, the only thing I&rsquo;d ask is that you actually measure before and after. My migration was right. The Valibot README is right. Both could still be wrong for your app. Measure. Then decide.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Valibot&#8217;s bundle size win is real, but switching from Zod cost me more than I expected. Here&#8217;s what I migrated, what I didn&#8217;t, and why I stopped halfway.<\/p>\n","protected":false},"author":2,"featured_media":173,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"Valibot's bundle size win is real, but switching from Zod cost me more than I expected. Here's what I migrated, what I didn't, and why I stopped halfway.","rank_math_focus_keyword":"valibot","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[156,35],"tags":[192,63,189,191,190],"class_list":["post-174","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-typescript","category-web-development","tag-bundle-size","tag-typescript","tag-valibot","tag-validation","tag-zod"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/174","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=174"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/174\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/173"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=174"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=174"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=174"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}