<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Zod on RockB</title><link>https://baeseokjae.github.io/tags/zod/</link><description>Recent content in Zod on RockB</description><image><title>RockB</title><url>https://baeseokjae.github.io/images/og-default.png</url><link>https://baeseokjae.github.io/images/og-default.png</link></image><generator>Hugo</generator><language>en-us</language><lastBuildDate>Mon, 08 Jun 2026 19:49:09 +0000</lastBuildDate><atom:link href="https://baeseokjae.github.io/tags/zod/index.xml" rel="self" type="application/rss+xml"/><item><title>Zod v4 TypeScript Schema Validation: What Changed and Migration Guide</title><link>https://baeseokjae.github.io/posts/zod-v4-schema-validation-typescript-guide-2026/</link><pubDate>Mon, 08 Jun 2026 19:49:09 +0000</pubDate><guid>https://baeseokjae.github.io/posts/zod-v4-schema-validation-typescript-guide-2026/</guid><description>Complete guide to Zod v4 breaking changes, 14x performance gains, new features, and a step-by-step migration path from Zod v3.</description><content:encoded><![CDATA[<p>Zod v4 is a major overhaul of the most popular TypeScript schema validation library — delivering 14x faster string parsing, a 57% smaller core bundle, and a completely reworked API for format validation. Most codebases can migrate in under a day using the official codemod, but there are real breaking changes that will catch you off guard if you skip the changelog.</p>
<h2 id="what-is-zod-v4-and-why-the-major-version-bump">What Is Zod v4 and Why the Major Version Bump?</h2>
<p>Zod v4 is a ground-up rewrite of Zod&rsquo;s internal architecture, released by Colin McDonnell in mid-2025 after two years of development driven by feedback from Zod v3&rsquo;s performance and bundle size limitations. With over 42,835 GitHub stars and 102 million weekly npm downloads, Zod is the de facto TypeScript runtime validation standard — and v4 is the release that finally addresses the criticisms Valibot raised in 2023. The major version bump is justified: v4 ships real breaking changes to string format methods, object strictness options, and error handling. But beyond compatibility breaks, v4 introduces architectural changes — a new <code>zod/v4/core</code> sub-package, the <code>@zod/mini</code> tree-shakable distribution, a metadata registry system, and first-class JSON Schema conversion — that change what you can build with Zod. This isn&rsquo;t just a performance patch; it&rsquo;s a new foundation for the library&rsquo;s next five years.</p>
<h3 id="why-the-breaking-changes-were-necessary">Why the Breaking Changes Were Necessary</h3>
<p>The Zod v3 API accumulated technical debt around how string format validators were attached to the <code>z.string()</code> chain. Methods like <code>.email()</code>, <code>.uuid()</code>, <code>.url()</code>, and <code>.ip()</code> were implemented as string refinements, which blocked tree-shaking and forced all format logic into the core bundle even for schemas that never use email validation. Moving these to top-level functions (<code>z.email()</code>, <code>z.uuid()</code>) was the only architecturally sound path to enabling the <code>@zod/mini</code> 1.9 KB distribution.</p>
<h2 id="performance-improvements--14x-faster-parsing-and-a-57-smaller-bundle">Performance Improvements — 14x Faster Parsing and a 57% Smaller Bundle</h2>
<p>Zod v4 is dramatically faster than v3 across all primitive types: 14x faster string parsing, 7x faster array parsing, and 6.5x faster object parsing, according to official benchmarks from the Zod v4 release page. On a realistic 50+ field schema parsing 1,000 objects, the improvement reaches 100x — dropping from 480ms in v3 to 4.8ms in v4. The core bundle is 57% smaller (2.3x), and TypeScript compile-time performance improves by up to 10x on large codebases. These numbers come from a complete rewrite of the parsing engine, which eliminated redundant type-checking layers, replaced recursive traversal with iterative loops, and pushed format validation out of the hot path. For most production applications, the cold-start and hot-path latency improvements will be noticeable immediately after upgrading. The smaller bundle also means less JavaScript for browsers to parse and execute, directly improving frontend performance metrics.</p>
<h3 id="what-drives-the-speed-gains">What Drives the Speed Gains</h3>
<p>Three architectural decisions explain the performance jump:</p>
<ol>
<li><strong>Eliminated double validation</strong>: v3 ran type checks twice in some paths; v4 does a single pass.</li>
<li><strong>Iterative instead of recursive</strong>: deeply nested schemas no longer risk stack overflows, and iterative traversal is faster in V8.</li>
<li><strong>Lazy format validation</strong>: <code>z.string()</code> no longer bundles email/url/uuid logic unless you explicitly import <code>z.email()</code>.</li>
</ol>
<h2 id="breaking-changes-you-must-know-before-migrating">Breaking Changes You Must Know Before Migrating</h2>
<p>Zod v4 ships seven categories of breaking changes that will cause runtime errors or type errors if not addressed. The most impactful changes are: string format methods removed from <code>z.string()</code> chain, object strictness helpers deprecated, and <code>ZodError</code> constructor unified. Before touching your codebase, run <code>npx zod-v3-to-v4</code> to catch the machine-fixable issues automatically.</p>
<p><strong>String format methods moved to top-level functions:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// Zod v3
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">emailSchema</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">string</span>().<span style="color:#a6e22e">email</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">urlSchema</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">string</span>().<span style="color:#a6e22e">url</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">uuidSchema</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">string</span>().<span style="color:#a6e22e">uuid</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Zod v4
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">emailSchema</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#a6e22e">email</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">urlSchema</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#a6e22e">url</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">uuidSchema</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#a6e22e">uuid</span>();
</span></span></code></pre></div><p><strong>Object strictness methods deprecated:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// Zod v3
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">schema</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">object</span>({ <span style="color:#a6e22e">name</span>: <span style="color:#66d9ef">z.string</span>() }).<span style="color:#a6e22e">strict</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">schema2</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">object</span>({ <span style="color:#a6e22e">name</span>: <span style="color:#66d9ef">z.string</span>() }).<span style="color:#a6e22e">passthrough</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Zod v4
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">schema</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#a6e22e">strictObject</span>({ <span style="color:#a6e22e">name</span>: <span style="color:#66d9ef">z.string</span>() });
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">schema2</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#a6e22e">looseObject</span>({ <span style="color:#a6e22e">name</span>: <span style="color:#66d9ef">z.string</span>() });
</span></span></code></pre></div><p><strong>Error handling unified — single parameter for custom errors:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// Zod v3
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">string</span>().<span style="color:#a6e22e">min</span>(<span style="color:#ae81ff">3</span>, { <span style="color:#a6e22e">message</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;Too short&#34;</span> });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Zod v4
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">string</span>().<span style="color:#a6e22e">min</span>(<span style="color:#ae81ff">3</span>, <span style="color:#e6db74">&#34;Too short&#34;</span>); <span style="color:#75715e">// string shorthand works
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">string</span>().<span style="color:#a6e22e">min</span>(<span style="color:#ae81ff">3</span>, { <span style="color:#a6e22e">error</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;Too short&#34;</span> }); <span style="color:#75715e">// &#39;message&#39; → &#39;error&#39;
</span></span></span></code></pre></div><p><strong><code>z.discriminatedUnion()</code> requires literal discriminators</strong> — computed or template literal discriminators from v3 will need refactoring.</p>
<p><strong><code>ZodError.issues</code> path format changed</strong> for nested arrays — update any custom error display logic that walks issue paths.</p>
<h3 id="breaking-change-impact-by-project-type">Breaking Change Impact by Project Type</h3>
<table>
  <thead>
      <tr>
          <th>Project Type</th>
          <th>Most-Affected APIs</th>
          <th>Risk Level</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Form validation (react-hook-form)</td>
          <td><code>.email()</code>, <code>.url()</code>, error messages</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>API validation (Express/Fastify)</td>
          <td><code>.strict()</code>, <code>discriminatedUnion</code></td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>OpenAPI/tRPC schemas</td>
          <td>Nearly all, but codemod covers most</td>
          <td>Low-Medium</td>
      </tr>
      <tr>
          <td>CLI tools</td>
          <td>Object strictness</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Frontend-only (switching to @zod/mini)</td>
          <td>Full API surface</td>
          <td>High</td>
      </tr>
  </tbody>
</table>
<h2 id="new-features-in-zod-v4--what-you-actually-gain">New Features in Zod v4 — What You Actually Gain</h2>
<p>Zod v4 adds capabilities that were impossible or impractical in v3: template literal type schemas, bidirectional JSON Schema conversion, a metadata registry, prettified error output, and the <code>@zod/mini</code> lightweight distribution. Template literal schemas (<code>z.templateLiteral</code>) let you validate strings like <code>&quot;event:${string}&quot;</code> with full TypeScript type inference — a feature previously only available in libraries like ArkType. JSON Schema conversion (<code>z.toJSONSchema()</code> / <code>z.fromJSONSchema()</code>) enables Zod to serve as the source of truth for OpenAPI specs without a separate schema definition layer. The metadata system (<code>z.globalRegistry</code>) lets you attach arbitrary metadata — descriptions, examples, deprecation flags — to any schema and access it at runtime. These aren&rsquo;t minor additions; they close the gap between Zod and more specialized validation tools for API contract workflows.</p>
<h3 id="new-error-formatting-api">New Error Formatting API</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// Prettified errors — human-readable output for debugging
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">result</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">object</span>({ <span style="color:#a6e22e">name</span>: <span style="color:#66d9ef">z.string</span>(), <span style="color:#a6e22e">age</span>: <span style="color:#66d9ef">z.number</span>() }).<span style="color:#a6e22e">safeParse</span>({ <span style="color:#a6e22e">name</span>: <span style="color:#66d9ef">123</span>, <span style="color:#a6e22e">age</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;old&#34;</span> });
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">result</span>.<span style="color:#a6e22e">success</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">z</span>.<span style="color:#a6e22e">prettifyError</span>(<span style="color:#a6e22e">result</span>.<span style="color:#a6e22e">error</span>));
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// ✕ name: Expected string, received number
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// ✕ age: Expected number, received string
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><h3 id="template-literal-types">Template Literal Types</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// Match &#34;event:click&#34;, &#34;event:hover&#34;, etc. with full type safety
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">eventSchema</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#a6e22e">templateLiteral</span>([<span style="color:#e6db74">&#34;event:&#34;</span>, <span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">string</span>()]);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Event</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">infer</span>&lt;<span style="color:#f92672">typeof</span> <span style="color:#a6e22e">eventSchema</span>&gt;; <span style="color:#75715e">// `event:${string}`
</span></span></span></code></pre></div><h3 id="json-schema-round-trip">JSON Schema Round-Trip</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">z</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#34;zod&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">userSchema</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">object</span>({
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">z.uuid</span>(),
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">email</span>: <span style="color:#66d9ef">z.email</span>(),
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">age</span>: <span style="color:#66d9ef">z.number</span>().<span style="color:#a6e22e">int</span>().<span style="color:#a6e22e">min</span>(<span style="color:#ae81ff">0</span>).<span style="color:#a6e22e">max</span>(<span style="color:#ae81ff">150</span>)
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">jsonSchema</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#a6e22e">toJSONSchema</span>(<span style="color:#a6e22e">userSchema</span>);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// { type: &#34;object&#34;, properties: { id: { type: &#34;string&#34;, format: &#34;uuid&#34; }, ... } }
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// And back
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">recovered</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#a6e22e">fromJSONSchema</span>(<span style="color:#a6e22e">jsonSchema</span>);
</span></span></code></pre></div><h2 id="zodmini--the-lightweight-19-kb-gzipped-distribution">@zod/mini — The Lightweight 1.9 KB Gzipped Distribution</h2>
<p><code>@zod/mini</code> is a separate npm package providing a tree-shakable subset of Zod v4 that weighs approximately 1.9 KB gzipped — compared to the full Zod v4 core which, while 57% smaller than v3, still ships more validation infrastructure than frontend bundles typically need. The package shares internal implementation with Zod v4 via the <code>zod/v4/core</code> sub-package, so it&rsquo;s not a fork — schemas created with <code>@zod/mini</code> are fully compatible with Zod v4 schemas at the type level. The API difference is that <code>@zod/mini</code> replaces method-chaining refinements with standalone function wrappers: instead of <code>z.string().min(3)</code>, you write <code>z.string(z.minLength(3))</code>. This change is what enables proper tree-shaking in bundlers, since each validation function is a discrete import that dead-code elimination can remove. For frontend applications that currently import the full Zod package just for form validation, switching to <code>@zod/mini</code> can meaningfully reduce bundle sizes.</p>
<h3 id="zodmini-api-pattern">@zod/mini API Pattern</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">z</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#34;@zod/mini&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// @zod/mini uses function composition instead of method chaining
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">emailSchema</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">string</span>(<span style="color:#a6e22e">z</span>.<span style="color:#a6e22e">email</span>(), <span style="color:#a6e22e">z</span>.<span style="color:#a6e22e">maxLength</span>(<span style="color:#ae81ff">100</span>));
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">userSchema</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">object</span>({
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">email</span>: <span style="color:#66d9ef">emailSchema</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">age</span>: <span style="color:#66d9ef">z.number</span>(<span style="color:#a6e22e">z</span>.<span style="color:#a6e22e">int</span>(), <span style="color:#a6e22e">z</span>.<span style="color:#a6e22e">gte</span>(<span style="color:#ae81ff">0</span>), <span style="color:#a6e22e">z</span>.<span style="color:#a6e22e">lte</span>(<span style="color:#ae81ff">150</span>))
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><h3 id="zod-vs-zodmini-vs-valibot-bundle-size">Zod vs @zod/mini vs Valibot Bundle Size</h3>
<table>
  <thead>
      <tr>
          <th>Library</th>
          <th>Bundle (gzipped)</th>
          <th>API Style</th>
          <th>TypeScript Inference</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Zod v3</td>
          <td>~14 KB</td>
          <td>Method chain</td>
          <td>Full</td>
      </tr>
      <tr>
          <td>Zod v4 core</td>
          <td>~6 KB</td>
          <td>Method chain</td>
          <td>Full</td>
      </tr>
      <tr>
          <td>@zod/mini</td>
          <td>~1.9 KB</td>
          <td>Function composition</td>
          <td>Full</td>
      </tr>
      <tr>
          <td>Valibot (simple form)</td>
          <td>~1.4 KB</td>
          <td>Function composition</td>
          <td>Full</td>
      </tr>
  </tbody>
</table>
<h2 id="step-by-step-migration-guide-from-zod-v3-to-v4">Step-by-Step Migration Guide from Zod v3 to v4</h2>
<p>Migrating from Zod v3 to v4 takes most projects under a day. The recommended sequence is: run the codemod first, fix TypeScript errors, then test your error handling paths. Start by installing Zod v4 — it&rsquo;s still published to the <code>zod</code> package, so <code>npm install zod@^4.0.0</code> is the only installation step. The codemod handles the highest-volume changes (string format methods, object strictness methods) automatically. Manual work is limited to: custom error message keys (<code>message</code> → <code>error</code>), any code that directly constructs <code>ZodError</code> objects, and custom discriminated union patterns that relied on non-literal discriminators.</p>
<p><strong>Step 1 — Update the package:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm install zod@^4.0.0
</span></span><span style="display:flex;"><span><span style="color:#75715e"># or</span>
</span></span><span style="display:flex;"><span>yarn add zod@^4.0.0
</span></span><span style="display:flex;"><span><span style="color:#75715e"># or</span>
</span></span><span style="display:flex;"><span>pnpm add zod@^4.0.0
</span></span></code></pre></div><p><strong>Step 2 — Run the codemod:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npx zod-v3-to-v4
</span></span></code></pre></div><p><strong>Step 3 — Fix remaining TypeScript errors:</strong></p>
<p>After the codemod, run <code>tsc --noEmit</code> and address any remaining type errors. Common remaining issues:</p>
<ul>
<li>Custom <code>ZodError</code> construction — update to new constructor signature</li>
<li><code>z.discriminatedUnion()</code> with non-literal discriminators — replace with union + refinement</li>
<li><code>.superRefine()</code> callback signature changes — second parameter is now a context object</li>
</ul>
<p><strong>Step 4 — Update error message keys:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// Search-replace: message: → error: in Zod option objects
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// Before
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">string</span>().<span style="color:#a6e22e">min</span>(<span style="color:#ae81ff">1</span>, { <span style="color:#a6e22e">message</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;Required&#34;</span> })
</span></span><span style="display:flex;"><span><span style="color:#75715e">// After
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">z</span>.<span style="color:#66d9ef">string</span>().<span style="color:#a6e22e">min</span>(<span style="color:#ae81ff">1</span>, { <span style="color:#a6e22e">error</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;Required&#34;</span> })
</span></span></code></pre></div><p><strong>Step 5 — Test error paths:</strong></p>
<p>Run your form submission tests and API validation tests with invalid inputs. Zod v4 formats errors differently in some edge cases — snapshot tests on error shapes may need updating.</p>
<h2 id="using-the-zod-v3-to-v4-automated-codemod">Using the zod-v3-to-v4 Automated Codemod</h2>
<p>The <code>zod-v3-to-v4</code> codemod is the fastest path through the mechanical parts of the Zod v4 migration. It uses jscodeshift to perform AST-level transforms — not regex substitution — so it handles complex cases like chained method calls inside ternaries, destructured schema assignments, and re-exported schemas correctly. Run it with <code>npx zod-v3-to-v4</code> from your project root; it automatically scans <code>src/</code> and <code>lib/</code> directories. The <code>--dry-run</code> flag prints what would change without modifying files, useful for reviewing scope before committing. The codemod covers: <code>.email()</code> → <code>z.email()</code>, <code>.url()</code> → <code>z.url()</code>, <code>.uuid()</code> → <code>z.uuid()</code>, <code>.ip()</code> → <code>z.ip()</code>, <code>.cuid()</code> → <code>z.cuid()</code>, <code>.nanoid()</code> → <code>z.nanoid()</code>, <code>.strict()</code> → <code>z.strictObject()</code>, <code>.passthrough()</code> → <code>z.looseObject()</code>, and <code>.strip()</code> → <code>z.object()</code>. It does NOT cover: <code>message</code> → <code>error</code> key renames in option objects, discriminated union discriminator changes, or custom error class construction.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Dry run first</span>
</span></span><span style="display:flex;"><span>npx zod-v3-to-v4 --dry-run
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Apply to src directory</span>
</span></span><span style="display:flex;"><span>npx zod-v3-to-v4 src/
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Apply to all TypeScript files in project</span>
</span></span><span style="display:flex;"><span>npx zod-v3-to-v4 --extensions<span style="color:#f92672">=</span>ts,tsx
</span></span></code></pre></div><h3 id="what-the-codemod-transforms-and-what-it-doesnt">What the Codemod Transforms (and What It Doesn&rsquo;t)</h3>
<table>
  <thead>
      <tr>
          <th>Transform</th>
          <th>Codemod Handles</th>
          <th>Manual Required</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>.email()</code> → <code>z.email()</code></td>
          <td>Yes</td>
          <td>No</td>
      </tr>
      <tr>
          <td><code>.url()</code> → <code>z.url()</code></td>
          <td>Yes</td>
          <td>No</td>
      </tr>
      <tr>
          <td><code>.uuid()</code> → <code>z.uuid()</code></td>
          <td>Yes</td>
          <td>No</td>
      </tr>
      <tr>
          <td><code>.strict()</code> → <code>z.strictObject()</code></td>
          <td>Yes</td>
          <td>No</td>
      </tr>
      <tr>
          <td><code>.passthrough()</code> → <code>z.looseObject()</code></td>
          <td>Yes</td>
          <td>No</td>
      </tr>
      <tr>
          <td><code>message:</code> → <code>error:</code> in options</td>
          <td>No</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td>Discriminated union discriminators</td>
          <td>No</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td><code>ZodError</code> constructor changes</td>
          <td>No</td>
          <td>Yes</td>
      </tr>
  </tbody>
</table>
<h2 id="zod-v4-vs-valibot-and-arktype--which-should-you-choose">Zod v4 vs Valibot and ArkType — Which Should You Choose?</h2>
<p>Zod v4 is the right choice for most TypeScript projects in 2026, but it&rsquo;s not the universal answer. Valibot remains meaningfully smaller for simple frontend schemas — a basic login form validation with Valibot is still ~90% smaller than Zod standard and ~73% smaller than Zod Mini, according to benchmarks from pkgpulse.com. ArkType offers the most advanced type inference in the ecosystem, supporting runtime-defined types with TypeScript-level fidelity that Zod&rsquo;s structural approach can&rsquo;t match. The practical decision comes down to three variables: bundle budget, ecosystem integration (tRPC, react-hook-form, Prisma, OpenAPI generators all have Zod adapters), and team familiarity. For any project already using Zod v3, upgrading to v4 is the obvious call. For greenfield frontend-only projects with strict bundle constraints, Valibot is worth evaluating. For complex domain type modeling where correctness matters more than ecosystem breadth, ArkType is the specialist choice.</p>
<h3 id="feature-comparison-2026">Feature Comparison 2026</h3>
<table>
  <thead>
      <tr>
          <th>Feature</th>
          <th>Zod v4</th>
          <th>@zod/mini</th>
          <th>Valibot</th>
          <th>ArkType</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Bundle size (gzipped)</td>
          <td>~6 KB</td>
          <td>~1.9 KB</td>
          <td>~1.4 KB</td>
          <td>~5 KB</td>
      </tr>
      <tr>
          <td>Method chaining</td>
          <td>Yes</td>
          <td>No</td>
          <td>No</td>
          <td>No</td>
      </tr>
      <tr>
          <td>JSON Schema output</td>
          <td>Built-in</td>
          <td>Built-in</td>
          <td>Plugin</td>
          <td>Plugin</td>
      </tr>
      <tr>
          <td>Template literal types</td>
          <td>Yes</td>
          <td>Yes</td>
          <td>No</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td>tRPC integration</td>
          <td>Native</td>
          <td>Partial</td>
          <td>Via adapter</td>
          <td>Via adapter</td>
      </tr>
      <tr>
          <td>TypeScript inference quality</td>
          <td>Excellent</td>
          <td>Excellent</td>
          <td>Excellent</td>
          <td>Best-in-class</td>
      </tr>
      <tr>
          <td>Codemod available</td>
          <td>v3→v4</td>
          <td>—</td>
          <td>—</td>
          <td>—</td>
      </tr>
  </tbody>
</table>
<h2 id="should-you-upgrade-now--practical-decision-framework">Should You Upgrade Now? — Practical Decision Framework</h2>
<p>Upgrading to Zod v4 makes sense for the vast majority of Zod v3 users, but the timing depends on your dependency chain. As of June 2026, most major Zod consumers — tRPC, Prisma, react-hook-form resolvers, and popular OpenAPI generators — have released Zod v4 compatible versions. If you&rsquo;re on a well-maintained dependency stack and not on an extremely old Node.js version (Zod v4 requires Node 16+), the upgrade path is low-risk. The main reason to delay is if your project has deep integrations with third-party libraries that haven&rsquo;t yet published Zod v4 support — check their issue trackers before upgrading. The performance gains are real and not marginal: if you&rsquo;re doing server-side validation on high-traffic routes, the 7–14x speedup translates directly to lower latency and reduced compute costs. For frontend projects, the smaller bundle is valuable even before reaching @zod/mini. The breaking changes are real but manageable — run the codemod, budget two to four hours for manual fixes, and test your error handling paths.</p>
<h3 id="upgrade-decision-matrix">Upgrade Decision Matrix</h3>
<table>
  <thead>
      <tr>
          <th>Your Situation</th>
          <th>Recommendation</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Zod v3 on active project, no unusual dependencies</td>
          <td>Upgrade now</td>
      </tr>
      <tr>
          <td>Zod v3 on project with custom ZodError handling</td>
          <td>Upgrade, budget 4h for manual work</td>
      </tr>
      <tr>
          <td>Zod v3 with niche third-party Zod adapters</td>
          <td>Check adapter compatibility first</td>
      </tr>
      <tr>
          <td>New project starting fresh</td>
          <td>Use Zod v4 from day one</td>
      </tr>
      <tr>
          <td>Frontend-only, bundle size critical</td>
          <td>Evaluate @zod/mini or Valibot</td>
      </tr>
      <tr>
          <td>Node 14 or earlier</td>
          <td>Stay on v3 or upgrade Node first</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="faq">FAQ</h2>
<p><strong>Q: Is Zod v4 backward-compatible with Zod v3 schemas?</strong></p>
<p>No, Zod v4 has real breaking changes. The most common are: string format methods (<code>.email()</code>, <code>.url()</code>, <code>.uuid()</code>) moved to top-level functions, object strictness helpers (<code>.strict()</code>, <code>.passthrough()</code>) renamed to <code>z.strictObject()</code> / <code>z.looseObject()</code>, and error option keys changed from <code>message</code> to <code>error</code>. The official codemod (<code>npx zod-v3-to-v4</code>) handles the high-volume transforms automatically, but some manual work remains.</p>
<p><strong>Q: Do I need to rewrite my schemas for @zod/mini?</strong></p>
<p>Yes, <code>@zod/mini</code> uses a different API style — function composition instead of method chaining. You cannot simply swap the import; you need to rewrite method chains like <code>z.string().min(3).email()</code> to the function composition equivalent <code>z.string(z.minLength(3), z.email())</code>. Consider <code>@zod/mini</code> only if you&rsquo;re starting a new project or doing a purposeful frontend bundle optimization pass.</p>
<p><strong>Q: Does Zod v4 work with tRPC?</strong></p>
<p>Yes, as of tRPC v11 (released Q4 2025), Zod v4 is fully supported. If you&rsquo;re on an older tRPC version, upgrade tRPC alongside Zod.</p>
<p><strong>Q: How long does migration from Zod v3 to v4 take?</strong></p>
<p>Most projects take two to eight hours. Run the codemod first (30 minutes), then address TypeScript errors (<code>tsc --noEmit</code>), then run your test suite. The main time sinks are: custom error message handling that used <code>{ message: &quot;...&quot; }</code> syntax (now <code>{ error: &quot;...&quot; }</code>), any code that directly instantiates <code>ZodError</code>, and discriminated union schemas with non-literal discriminators.</p>
<p><strong>Q: Is Zod v4 faster for all schema types?</strong></p>
<p>The published benchmarks show 14x faster string parsing, 7x faster array parsing, and 6.5x faster object parsing. These are best-case numbers; real-world gains depend on schema complexity and input characteristics. Schemas with many <code>z.refine()</code> calls or async validators will see smaller gains since those paths weren&rsquo;t the primary optimization target. For typical REST API request validation with simple object/string/number types, expect gains in the 4–10x range in practice.</p>
]]></content:encoded></item></channel></rss>