1<!DOCTYPE html><html data-dpl-id="dpl_Bvcw8bBncaxS6Jx1uCbi7fdD3tqx"><head><meta charSet="utf-8" data-next-head=""/><meta name="viewport" content="width=device-width" data-next-head=""/>
1<script async="" src="https://platform.twitter.com/widgets.js" data-charset="utf-8" data-next-head=""></script>
1<script async="" defer="" src="https://buttons.github.io/buttons.js"></script>
1<meta name="msapplication-TileColor" content="#ffffff" class="jsx-1725f8811803089d" data-next-head=""/><meta name="msapplication-TileImage" content="/ms-icon-144x144.png" class="jsx-1725f8811803089d" data-next-head=""/><meta name="theme-color" content="#ffffff" class="jsx-1725f8811803089d" data-next-head=""/><title data-next-head="">Introducing Zod Codecs</title><meta name="copyright" content="Colin McDonnell" data-next-head=""/><link rel="canonical" href="https://colinhacks.com/essays/introducing-zod-codecs" data-next-head=""/><meta property="og:type" content="website" data-next-head=""/><meta name="og:title" property="og:title" content="Introducing Zod Codecs" data-next-head=""/><meta property="og:site_name" content="Colin McDonnell @colinhacks" data-next-head=""/><meta property="og:url" content="https://colinhacks.com/essays/introducing-zod-codecs" data-next-head=""/><meta name="twitter:card" content="summary_large_image" data-next-head=""/><meta name="twitter:title" content="Introducing Zod Codecs" data-next-head=""/><meta name="twitter:site" content="@colinhacks" data-next-head=""/><meta name="twitter:creator" content="@colinhacks" data-next-head=""/><meta name="twitter:image" content="https://colinhacks.com/codec-neon.png" data-next-head=""/><meta property="og:image" content="https://colinhacks.com/codec-neon.png" data-next-head=""/><link rel="preconnect" href="https://fonts.googleapis.com"/><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous"/><link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png"/><link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png"/><link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png"/><link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png"/><link rel="apple-touch-icon" sizes="114x114" href="/apple-icon-114x114.png"/><link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120x120.png"/><link rel="apple-touch-icon" sizes="144x144" href="/apple-icon-144x144.png"/><link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152x152.png"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png"/><link rel="icon" type="image/png" sizes="192x192" href="/android-icon-192x192.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json"/><link rel="preload" href="/_next/static/chunks/10yqoss7_xy-_.css?dpl=dpl_Bvcw8bBncaxS6Jx1uCbi7fdD3tqx" as="style"/><link href="https://fonts.googleapis.com/css2?family=Encode+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/><link rel="stylesheet" href="/_next/static/chunks/10yqoss7_xy-_.css?dpl=dpl_Bvcw8bBncaxS6Jx1uCbi7fdD3tqx" data-n-g=""/><noscript data-n-css=""></noscript>
1<script src="/_next/static/chunks/3mogii3dpt0fb.js?dpl=dpl_Bvcw8bBncaxS6Jx1uCbi7fdD3tqx" defer=""></script>
1<script src="/_next/static/chunks/3sq8imzsw6fxs.js?dpl=dpl_Bvcw8bBncaxS6Jx1uCbi7fdD3tqx" defer=""></script>
1<script src="/_next/static/chunks/turbopack-1feq9bd1d7522.js?dpl=dpl_Bvcw8bBncaxS6Jx1uCbi7fdD3tqx" defer=""></script>
1<script src="/_next/static/chunks/120nlq36ctg1_.js?dpl=dpl_Bvcw8bBncaxS6Jx1uCbi7fdD3tqx" defer=""></script>
1<script src="/_next/static/chunks/0bvzlsoqmbjub.js?dpl=dpl_Bvcw8bBncaxS6Jx1uCbi7fdD3tqx" defer=""></script>
1<script src="/_next/static/chunks/3ezrp91eegsu5.js?dpl=dpl_Bvcw8bBncaxS6Jx1uCbi7fdD3tqx" defer=""></script>
1<script src="/_next/static/chunks/3on531t617l4g.js?dpl=dpl_Bvcw8bBncaxS6Jx1uCbi7fdD3tqx" defer=""></script>
1<script src="/_next/static/chunks/turbopack-3h-nqtzu04q_x.js?dpl=dpl_Bvcw8bBncaxS6Jx1uCbi7fdD3tqx" defer=""></script>
1<script src="/_next/static/build-TfctsWXpff2fKS/_buildManifest.js?dpl=dpl_Bvcw8bBncaxS6Jx1uCbi7fdD3tqx" defer=""></script>
1<script src="/_next/static/build-TfctsWXpff2fKS/_ssgManifest.js?dpl=dpl_Bvcw8bBncaxS6Jx1uCbi7fdD3tqx" defer=""></script>
1<script src="/_next/static/build-TfctsWXpff2fKS/_clientMiddlewareManifest.js?dpl=dpl_Bvcw8bBncaxS6Jx1uCbi7fdD3tqx" defer=""></script>
1<style id="__jsx-275caf0cd930a3b">.devii-markdown p{margin:0;padding:10px 0;line-height:1.7}.devii-markdown pre{border-radius:7px}.devii-markdown>p{margin:0;padding:10px 0}.devii-markdown>p>img{border-radius:8px;width:100%;box-shadow:0 4px 30px #00000040}.devii-markdown ul,.devii-markdown ol{margin:8px 0 20px;padding-left:22px;line-height:1.7}.devii-markdown>p,.devii-markdown ul>li,.devii-markdown ol>li{color:#171717}.devii-markdown>ul>li,.devii-markdown>ol>li{margin:12px;padding:0}.devii-markdown li>p{margin-top:12px;margin-bottom:12px;padding:0}.devii-markdown>h1,.devii-markdown>h2,.devii-markdown>h3,.devii-markdown>h4,.devii-markdown>h5,.devii-markdown>h6{color:#171717;margin:0;padding:0;font-weight:700;line-height:1.3}.devii-markdown>h1{font-size:2em}.devii-markdown>h2{font-size:1.5em}.devii-markdown>h3{font-size:1.2em}.devii-markdown>h4{font-size:1.05em}.devii-markdown>h1>a,.devii-markdown>h2>a,.devii-markdown>h3>a,.devii-markdown>h4>a,.devii-markdown>h5>a,.devii-markdown>h6>a{text-decoration:none}.devii-markdown>hr{opacity:.35;margin:20px 0}.devii-markdown>h1{padding-top:40px;padding-bottom:20px}.devii-markdown>h2{padding-top:30px;padding-bottom:20px}.devii-markdown>h3{padding-top:20px;padding-bottom:15px}.devii-markdown>h4{padding-top:15px;padding-bottom:10px}.devii-markdown>h5,.devii-markdown>h6{padding-top:10px;padding-bottom:10px}.devii-markdown a{color:#142244;text-underline-offset:4px;-webkit-text-decoration:underline #ff6f7db3;text-decoration:underline #ff6f7db3;text-decoration-thickness:2px;transition:color 15ms linear,text-decoration-color 15ms linear}.devii-markdown a:hover{color:#ec556a;text-decoration-color:#ff6f7d}.devii-markdown code{background-color:#00000010;border-radius:2px;padding:3px;font-size:91%}.devii-markdown pre{font-size:11pt;margin:20px 0!important;padding:20px!important}.devii-markdown pre>code{background-color:#0000;border-radius:0;padding:0}.devii-markdown>blockquote{background-color:#f7f8fa;border:1px solid #e6e7eb;border-radius:12px;width:100%;margin:24px 0;padding:14px 20px}.devii-markdown>blockquote>p{color:#2b3a5c;opacity:1;margin:0;padding:2px 0;font-size:1rem;line-height:1.6}.devii-markdown>div.twitter-tweet{max-width:600px;margin:auto}</style><style id="__jsx-1725f8811803089d">html,body,#__next{min-height:100%;font-family:var(--font-sans);margin:0;padding:0}*{box-sizing:border-box}html p,html ol,html ul{font-size:12pt;line-height:1.3}h1,h2,h3,h4,h5,h6{font-family:var(--font-sans)}.postcard{opacity:.92;box-shadow:0 2px 10px #00000040}.postcard:hover{opacity:1;box-shadow:0 1px 3px #00000040}table{border-collapse:collapse;text-align:center}caption{caption-side:bottom;margin:4px;font-style:italic;font-weight:700}table,th,td{border:none}th,td{vertical-align:middle;height:24px;padding:6px 10px}tr:nth-child(odd){background-color:#eee}tr:first-child{background-color:#fff}</style><style id="emotion-styles" data-emotion-css="urnqzy 8atqhb 3jdg1i u4p24i">.css-urnqzy{width:100%;max-width:100%;margin:0px;}@media only screen and (min-width:768px){.css-urnqzy{margin-top:15px;border-radius:8px;box-shadow:0px 2px 12px #00000040;}}.css-8atqhb{width:100%;}.css-3jdg1i{margin:0px;padding:0px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;}.css-u4p24i{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}</style></head><body><link rel="preload" as="image" href="/codec-neon.png"/><link rel="preload" as="image" href="/codecs/codecs-network-dark.svg"/><link rel="preload" as="image" href="/codecs/codecs-dark.png"/><link rel="preload" as="image" href="/autodiscoverable-thumb.png"/><link rel="preload" as="image" href="/live-typescript-thumb.png"/><link rel="preload" as="image" href="/envelopes_small.jpg"/><div id="__next"><div style="display:flex;flex-direction:column;align-items:center;min-height:100vh" class="jsx-1725f8811803089d"><header class="sticky top-0 z-50 w-full border-b border-hairline/80 bg-paper/80 backdrop-blur-md"><div class="mx-auto flex h-14 w-full max-w-5xl items-center justify-between px-6"><a href="/" class="text-[15px] font-bold tracking-tight text-ink transition-colors duration-[15ms] hover:text-coral-deep">Colin McDonnell</a><nav class="flex items-center gap-5"><a href="/essays" class="text-[13.5px] font-medium text-muted transition-colors duration-[15ms] hover:text-coral-deep">Writing</a><a href="/#projects" class="text-[13.5px] font-medium text-muted transition-colors duration-[15ms] hover:text-coral-deep">Projects</a><a href="/about" class="text-[13.5px] font-medium text-muted transition-colors duration-[15ms] hover:text-coral-deep">About</a><a href="https://twitter.com/colinhacks" aria-label="X / Twitter" class="text-muted transition-colors duration-[15ms] hover:text-coral-deep"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="h-[16px] w-[16px]"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.66l-5.214-6.817-5.967 6.817H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231 5.45-6.231Zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77Z"></path></svg></a><a href="https://github.com/colinhacks" aria-label="GitHub" class="text-muted transition-colors duration-[15ms] hover:text-coral-deep"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="h-[18px] w-[18px]"><path d="M12 .5C5.73.5.5 5.73.5 12.02c0 5.1 3.29 9.41 7.86 10.94.58.11.79-.25.79-.56 0-.27-.01-1-.02-1.96-3.2.7-3.88-1.54-3.88-1.54-.53-1.35-1.29-1.71-1.29-1.71-1.05-.72.08-.71.08-.71 1.16.08 1.77 1.2 1.77 1.2 1.03 1.7
17 2.7 1.26 3.36.96.1-.75.4-1.26.73-1.55-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18.92-.26 1.91-.39 2.89-.39.98 0 1.97.13 2.89.39 2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.12 3.05.74.8 1.18 1.83 1.18 3.09 0 4.42-2.69 5.39-5.25 5.68.41.36.78 1.06.78 2.14 0 1.55-.01 2.8-.01 3.18 0 .31.21.68.8.56A11.53 11.53 0 0 0 23.5 12.02C23.5 5.73 18.27.5 12 .5Z"></path></svg></a></nav></div></header><main class="w-full"><article class="mx-auto w-full max-w-2xl px-6 pt-12 sm:pt-16"><div style="width:100%"><img class="css-urnqzy" src="/codec-neon.png"/><div style="height:40px"> </div><div class="css-8atqhb"><h1 class="text-[2rem] font-bold leading-tight tracking-tight text-ink" style="margin:10px 0px 10px 0px;padding:0;border:none">Introducing Zod Codecs</h1><div style="height:15px"> </div><div style="margin:0px;padding:0px"><div class="css-3jdg1i"><div><div class="css-u4p24i"><p style="padding:2px;margin:0;line-height:1.2;font-size:11pt">Colin McDonnell<!-- --> <a style="text-decoration:none;color:#ec556a" href="https://twitter.com/colinhacks">@colinhacks</a></p><div style="width:7px"> </div></div><p style="opacity:0.6;padding:2px;margin:0;line-height:1.2;font-size:11pt">published August 23rd, 2025</p></div></div></div><div style="height:50px"> </div><div style="width:100%" class="jsx-275caf0cd930a3b devii-markdown"><p>Zod 4.1 introduced a new <code>z.codec()</code> API for defining bi-directional transformations in Zod.</p><h2>The problem with transforms</h2><p>Zod's <code>.transform()</code> method is great for one-way data conversion:</p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> stringToNumber </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">string</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">transform</span><span class="token" style="color:#a9b7c6">(</span><span>val </span><span class="token arrow" style="color:#a9b7c6">=></span><span> </span><span class="token" style="color:#ffc66d">parseFloat</span><span class="token" style="color:#a9b7c6">(</span><span>val</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 2</span><span>stringToNumber</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">parse</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"42"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// 42</span></code></pre><p>But what if you need to go both ways? Say, you're storing dates as ISO strings in a database but want to work with <code>Date</code> objects in your app.</p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> stringToDate </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">string</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">transform</span><span class="token" style="color:#a9b7c6">(</span><span>str </span><span class="token arrow" style="color:#a9b7c6">=></span><span> </span><span class="token" style="color:#cc7832">new</span><span> </span><span class="token class-name known-class-name">Date</span><span class="token" style="color:#a9b7c6">(</span><span>str</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 3</span><span></span><span class="token" style="color:#cc7832">const</span><span> dateToString </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">date</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">transform</span><span class="token" style="color:#a9b7c6">(</span><span>date </span><span class="token arrow" style="color:#a9b7c6">=></span><span> date</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">toISOString</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 4</span> 5<span></span><span class="token" style="color:#808080">// Two separate schemas, manually kept in sync</span><span> 6</span><span>stringToDate</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">parse</span><span class="token" style="color:#a9b7c6">(</span>
6<span class="token" style="color:#6a8759">"2024-01-15T10:30:00.000Z"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// Date</span><span> 7</span><span>dateToString</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">parse</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#cc7832">new</span><span> </span><span class="token class-name known-class-name">Date</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// "2024-01-15T10:30:00.000Z"</span></code></pre><p>This works, but it's brittle. You need to keep track of two schemas and remember that they are intended as inverses. You need to manually verify that the output type of one matches the input type of the other. If you change one, you have to remember to update the other. </p><h2>Introducing codecs</h2><p>Codecs are a new Zod API for defining <em>bidirectional transformations</em> between two types. You specify an input schema, output schema, and transformation functions in both directions:</p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> stringToDate </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">codec</span><span class="token" style="color:#a9b7c6">(</span><span> 8</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token property-access">iso</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">datetime</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> </span><span class="token" style="color:#808080">// input schema: ISO string</span><span> 9</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">date</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> </span><span class="token" style="color:#808080">// output schema: Date object</span><span> 10</span><span> </span><span class="token" style="color:#a9b7c6">{</span><span> 11</span><span> </span><span class="token function-variable" style="color:#ffc66d">
11decode</span><span class="token" style="color:#a9b7c6">:</span><span> isoString </span><span class="token arrow" style="color:#a9b7c6">=></span><span> </span><span class="token" style="color:#cc7832">new</span><span> </span><span class="token class-name known-class-name">Date</span><span class="token" style="color:#a9b7c6">(</span><span>isoString</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> </span><span class="token" style="color:#808080">// string â Date</span><span> 12</span><span> </span><span class="token function-variable" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">:</span><span> date </span><span class="token arrow" style="color:#a9b7c6">=></span><span> date</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">toISOString</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> </span><span class="token" style="color:#808080">// Date â string</span><span> 13</span><span> </span><span class="token" style="color:#a9b7c6">}</span><span> 14</span><span></span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span></code></pre><p>You can process data in both directions using the new top-level <code>.decode()</code> and <code>.encode()</code> methods:</p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span>stringToDate</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">
14decode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"2024-01-15T10:30:00.000Z"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// Date</span><span> 15</span><span>stringToDate</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#cc7832">new</span><span> </span><span class="token class-name known-class-name">Date</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"2024-01-15"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// "2024-01-15T00:00:00.000Z"</span></code></pre><blockquote><p><strong>Note</strong>Â â For bundle size reasons, these new methods have not added to Zod Mini schemas. Instead, this functionality is available via equivalent top-level functions. </p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#808080">// equivalent at runtime</span><span> 16</span><span>z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">
16decode</span><span class="token" style="color:#a9b7c6">(</span><span>stringToDate</span><span class="token" style="color:#a9b7c6">,</span><span> </span><span class="token" style="color:#6a8759">"2024-01-15T10:30:00.000Z"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 17</span><span>z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span>stringToDate</span><span class="token" style="color:#a9b7c6">,</span><span> </span><span class="token" style="color:#cc7832">new</span><span> </span><span class="token class-name known-class-name">Date</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span></code></pre></blockquote><p>This is particularly important when you are using Zod to <em>map data</em> back and forth between two different domains. One common use case is to convert data to/from a serializable format like JSON into a richer JavaScript representation (with <code>Date</code>, <code>bigint</code>, etc).</p><p><img alt="Zod schemas shared" src="/codecs/codecs-network-dark.svg"/></p><h3>Async</h3><p>The transformation functions can be <code>async</code>.</p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> asyncCodec </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">codec</span><span class="token" style="color:#a9b7c6">(</span><span>z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">string</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">number</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> </span><span class="token" style="color:#a9b7c6">{</span><span> 18</span><span> </span><span class="token function-variable" style="color:#ffc66d">
18decode</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#cc7832">async</span><span> str </span><span class="token arrow" style="color:#a9b7c6">=></span><span> </span><span class="token known-class-name class-name">Number</span><span class="token" style="color:#a9b7c6">(</span><span>str</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> 19</span><span> </span><span class="token function-variable" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#cc7832">async</span><span> num </span><span class="token arrow" style="color:#a9b7c6">=></span><span> num</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">toString</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> 20</span><span></span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span></code></pre><p>The usual "safe" and "async" variants exist:</p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span>syncCodec</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"42"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 21</span><span>syncCodec</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">safeEncode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"42"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 22</span><span></span><span class="token control-flow" style="color:#cc7832">await</span><span> asyncCodec</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encodeAsync</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"42"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 23</span><span></span><span class="token control-flow" style="color:#cc7832">await</span><span> asyncCodec</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">safeEncodeAsync</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"42"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span></code></pre><h3>Composability</h3><p>Codecs can be composed inside other schemas, just like any other schema. There are no special rules.</p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span>
23 queryParams </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">object</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">{</span><span> 24</span><span> before</span><span class="token" style="color:#a9b7c6">:</span><span> stringToDate</span><span class="token" style="color:#a9b7c6">,</span><span> 25</span><span> after</span><span class="token" style="color:#a9b7c6">:</span><span> stringToDate 26</span><span></span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">)</span><span> 27</span> 28<span>queryParams</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">{</span><span> 29</span><span> before</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#cc7832">new</span><span> </span><span class="token class-name known-class-name">Date</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> 30</span><span> after</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#cc7832">new</span><span> </span><span class="token class-name known-class-name">Date</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span> 31</span><span></span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 32</span><span></span><span class="token" style="color:#808080">// => { before: string, after: string }</span></code></pre><h3><code>.parse()</code> vs <code>.decode()</code></h3><p>Let's compare the existing <code>.parse()</code> APIs to <code>.decode()</code>. <em><code>.parse()</code> is equivalent to <code>.decode()</code> at runtime.</em></p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#808080">// equivalent at runtime</span><span> 33</span><span>stringToDate</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">parse</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"2024-01-15T10:30:00.000Z"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 34</span><span>stringToDate</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">
34decode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"2024-01-15T10:30:00.000Z"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span></code></pre><p>Though they're identical at runtime, their type signatures differ in an important way. While <code>.parse()</code> accepts <code>unknown</code>, <code>decode</code> expects a <em>strongly-typed inputs</em>.</p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span>stringToDate</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">parse</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6897bb">12345</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 35</span><span></span><span class="token" style="color:#808080">// No TypeScript error but fails at runtime</span><span> 36</span> 37<span>stringToDate</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">
37decode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6897bb">12345</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 38</span><span></span><span class="token" style="color:#808080">// â TypeScript error: Argument of type 'number' is not assignable to parameter of type 'string'</span></code></pre><p>Here's a diagram demonstrating the differences:</p><p><img alt="Codec directionality diagram" src="/codecs/codecs-dark.png"/></p><blockquote><p>This is a highly requested feature unto itself.</p><ul><li><a href="https://github.com/colinhacks/zod/issues/3860">#3860</a> Add strongly typed parse function</li><li><a href="https://github.com/colinhacks/zod/issues/1748">#1748</a> Typed input for parse methods</li><li><a href="https://github.com/colinhacks/zod/issues/3978">#3978</a> Type-safe parsing with known input types</li><li><a href="https://github.com/colinhacks/zod/issues/1892">#1892</a> Strongly typed decode function</li></ul></blockquote><h2>How encoding works</h2><p>Most Zod schemas in the universe don't perform any kind of transformation. Their inferred input and output types are identical. For these schemas, there is no difference between parsing/decoding and encoding.</p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> mySchema </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">object</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">{</span><span> 39</span><span> name</span><span class="token" style="color:#a9b7c6">:</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">string</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span> 40</span><span></span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 41</span> 42<span></span><span class="token" style="color:#808080">// no difference</span><span> 43</span><span>mySchema</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">parse</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">{</span><span> name</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#6a8759">"colinhacks"</span><span> </span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 44</span><span>mySchema</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">
44decode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">{</span><span> name</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#6a8759">"colinhacks"</span><span> </span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">)</span><span> 45</span><span>mySchema</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">{</span><span> name</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#6a8759">"colinhacks"</span><span> </span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">)</span></code></pre><p>A small number of APIs cause the input and output types to diverge. In these scenarios, the runtime behavior of <code>.decode()</code>/<code>.encode()</code> also differ.</p><h3>Codecs</h3><p>This is an obvious one. During <code>.decode()</code>, the <code>decode</code> function runs. During <code>.encode()</code>, the <code>encode</code> function runs. Simple.</p><h3>Transforms â ï¸</h3><p>This is the #1 rule of <code>.encode()</code>: you can't use <code>.transform()</code>. That API is inherently unidirectional. If your schema contains any transforms, attempting an "encode" operation with it will throw a runtime error. You'll need to refactor to use <code>z.codec()</code>.</p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> schema </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">string</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">transform</span><span class="token" style="color:#a9b7c6">(</span><span>val </span><span class="token arrow" style="color:#a9b7c6">=></span><span> val</span><span class="token" style="color:#a9b7c6">.</span><span class="token property-access">length</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 46</span> 47<span>schema</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6897bb">5</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 48</span><span></span><span class="token" style="color:#808080">// â ZodEncodeError: Encountered unidirectional transform during encode</span></code></pre><h3>Pipes</h3><blockquote><p><strong>Note</strong> â Codecs are actually implemented as a subclass of <code>ZodPipe</code> augmented with "interstitial" transform logic.</p></blockquote><p>Pipes reverse their order during encoding, from <code>A â B</code> to <code>B â A</code>. That said, pipes are typically used in conjunction with transforms, so "vanilla" pipes are rarely useful in the context of encoding. Prefer <code>z.codec()</code> everywhere.</p><h3>
48Refinements</h3><p>All checks (<code>.refine()</code>, <code>.min()</code>, <code>.max()</code>, etc.) are still executed in both directions. </p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> schema </span><span class="token" style="color:#a9b7c6">=</span><span> stringToDate</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">refine</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">(</span><span>date</span><span class="token" style="color:#a9b7c6">)</span><span> </span><span class="token arrow" style="color:#a9b7c6">=></span><span> date</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">getFullYear</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span> </span><span class="token" style="color:#a9b7c6">></span><span> </span><span class="token" style="color:#6897bb">2000</span><span class="token" style="color:#a9b7c6">,</span><span> </span><span class="token" style="color:#6a8759">"Must be this millenium"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 49</span> 50<span>schema</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#cc7832">new</span><span> </span><span class="token class-name known-class-name">Date</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"2000-01-01"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 51</span><span></span><span class="token" style="color:#808080">// => Date</span><span> 52</span> 53<span>schema</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#cc7832">new</span><span> </span><span class="token class-name known-class-name">Date</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"1999-01-01"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 54</span><span></span><span class="token" style="color:#808080">// => â ZodError: [</span><span> 55</span><span></span><span class="token" style="color:#808080">// {</span><span> 56</span><span></span><span class="token" style="color:#808080">// "code": "custom",</span><span> 57</span><span></span><span class="token" style="color:#808080">// "path": [],</span><span> 58</span><span></span><span class="token" style="color:#808080">// "message": "Must be this millenium"</span><span> 59</span><span></span><span class="token" style="color:#808080">// }</span><span> 60</span><span></span><span class="token" style="color:#808080">// ]</span></code></pre><p>To avoid unexpected errors in your custom <code>.refine()</code> logic, Zod performs two "passes" during <code>.encode()</code>. The first pass ensures the input type conforms to the expected type (no <code>invalid_type</code> errors). If that passes, Zod performs the second pass which executes the refinement logic.</p><p>This approach means all parsing & refinement logic runs in exactly the reverse order during encoding. Even "mutating refinements" like <code>z.string().trim()</code> or <code>z.string().toLowerCase()</code> work as expected. </p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> schema </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">string</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">trim</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 61</span> 62<span>schema</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">
62decode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">" hello "</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 63</span><span></span><span class="token" style="color:#808080">// => "hello"</span><span> 64</span> 65<span>schema</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">" hello "</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 66</span><span></span><span class="token" style="color:#808080">// => "hello"</span></code></pre><h3>Default/prefault</h3><p>Default and prefault values are only applied in the forward direction. </p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> withDefault </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">string</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">.</span><span class="token module" style="color:#cc7832">default</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"hello"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 67</span> 68<span>withDefault</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">
68decode</span><span class="token" style="color:#a9b7c6">(</span><span class="token nil" style="color:#cc7832">undefined</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// "hello"</span><span> 69</span><span>withDefault</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token nil" style="color:#cc7832">undefined</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// â ZodError</span></code></pre><p>This is by design. When you add a default, the input becomes <code>string | undefined</code> but the output stays <code>string</code>. As such, <code>undefined</code> isn't considered a valid input to <code>.encode()</code>.</p><h3>Catch</h3><p>Similarly, <code>.catch()</code> values are only applied in the forward direction.</p><h3>Stringbool</h3><blockquote><p><strong>Note</strong> â <a href="https://zod.dev/api?id=stringbool">Stringbool</a> pre-dates the introduction of codecs in Zod. It has since been internally re-implemented as a codec. </p></blockquote><p>The <code>z.stringbool()</code> API converts string values (<code>"true"</code>, <code>"false"</code>, <code>"yes"</code>, <code>"no"</code>, etc.) into <code>boolean</code>. By default, it will convert <code>true</code> to <code>"true"</code> and <code>false</code> to <code>"false"</code> during <code>.encode()</code>..</p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> stringbool </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">stringbool</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 70</span> 71<span>stringbool</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">
71decode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"true"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// => true</span><span> 72</span><span>stringbool</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">decode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"false"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// => false</span><span> 73</span> 74<span>stringbool</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#cc7832">true</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// => "true"</span><span> 75</span><span>stringbool</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#cc7832">false</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// => "false"</span></code></pre><p>If you specify a custom set of <code>truthy</code> and <code>falsy</code> values, the <em>first element in the array</em> will be used instead.</p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> stringbool </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">stringbool</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">{</span><span> truthy</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#a9b7c6">[</span><span class="token" style="color:#6a8759">"yes"</span><span class="token" style="color:#a9b7c6">,</span><span> </span><span class="token" style="color:#6a8759">"y"</span><span class="token" style="color:#a9b7c6">]</span><span class="token" style="color:#a9b7c6">,</span><span> falsy</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#a9b7c6">[</span><span class="token" style="color:#6a8759">"no"</span><span class="token" style="color:#a9b7c6">,</span><span> </span><span class="token" style="color:#6a8759">"n"</span><span class="token" style="color:#a9b7c6">]</span><span> </span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 76</span> 77<span>stringbool</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#cc7832">true</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// => "yes"</span><span> 78</span><span>stringbool</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#cc7832">
78false</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// => "no"</span></code></pre><h2>Official codecs</h2><p>Zod doesn't provide any predefined codecs out of the box. Instead, the docs provide some "canonical" codec implementations you can copy/paste into your projects as needed. These have all been tested internally. </p><ul><li><a href="https://zod.dev/codecs?id=stringtonumber"><code>stringToNumber</code></a></li><li><a href="https://zod.dev/codecs?id=stringtoint"><code>stringToInt</code></a></li><li><a href="https://zod.dev/codecs?id=stringtobigint"><code>stringToBigInt</code></a></li><li><a href="https://zod.dev/codecs?id=numbertobigint"><code>numberToBigInt</code></a></li><li><a href="https://zod.dev/codecs?id=isodatetimetodate"><code>isoDatetimeToDate</code></a></li><li><a href="https://zod.dev/codecs?id=epochsecondstodate"><code>epochSecondsToDate</code></a></li><li><a href="https://zod.dev/codecs?id=epochmillistodate"><code>epochMillisToDate</code></a></li><li><a href="https://zod.dev/codecs?id=jsoncodec"><code>jsonCodec</code></a></li><li><a href="https://zod.dev/codecs?id=utf8tobytes"><code>utf8ToBytes</code></a></li><li><a href="https://zod.dev/codecs?id=bytestoutf8"><code>bytesToUtf8</code></a></li><li><a href="https://zod.dev/codecs?id=base64tobytes"><code>base64ToBytes</code></a></li><li><a href="https://zod.dev/codecs?id=base64urltobytes"><code>base64urlToBytes</code></a></li><li><a href="https://zod.dev/codecs?id=hextobytes"><code>hexToBytes</code></a></li><li><a href="https://zod.dev/codecs?id=stringtourl"><code>stringToURL</code></a></li><li><a href="https://zod.dev/codecs?id=stringtohttpurl"><code>stringToHttpURL</code></a></li><li><a href="https://zod.dev/codecs?id=uricomponent"><code>uriComponent</code></a></li><li><a href="https://zod.dev/codecs?id=stringtoboolean"><code>stringToBoolean</code></a></li></ul><p>Some selected examples are below.</p><h3><code>stringToBigInt</code></h3><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> stringToBigInt </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">codec</span><span class="token" style="color:#a9b7c6">(</span><span>z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">string</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">bigint</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> </span><span class="token" style="color:#a9b7c6">{</span><span> 79</span><span> </span><span class="token function-variable" style="color:#ffc66d">
79decode</span><span class="token" style="color:#a9b7c6">:</span><span> str </span><span class="token arrow" style="color:#a9b7c6">=></span><span> </span><span class="token known-class-name class-name">BigInt</span><span class="token" style="color:#a9b7c6">(</span><span>str</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> 80</span><span> </span><span class="token function-variable" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">:</span><span> bigint </span><span class="token arrow" style="color:#a9b7c6">=></span><span> bigint</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">toString</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> 81</span><span></span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 82</span> 83<span>stringToBigInt</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">
83decode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"12345"</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// 12345n</span><span> 84</span><span>stringToBigInt</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6897bb">12345n</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// "12345"</span></code></pre><h3><code>jsonCodec</code></h3><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> jsonCodec </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">codec</span><span class="token" style="color:#a9b7c6">(</span><span>z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">string</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">json</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> </span><span class="token" style="color:#a9b7c6">{</span><span> 85</span><span> </span><span class="token function-variable" style="color:#ffc66d">
85decode</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#a9b7c6">(</span><span>jsonString</span><span class="token" style="color:#a9b7c6">,</span><span> ctx</span><span class="token" style="color:#a9b7c6">)</span><span> </span><span class="token arrow" style="color:#a9b7c6">=></span><span> </span><span class="token" style="color:#a9b7c6">{</span><span> 86</span><span> </span><span class="token control-flow" style="color:#cc7832">try</span><span> </span><span class="token" style="color:#a9b7c6">{</span><span> 87</span><span> </span><span class="token control-flow" style="color:#cc7832">return</span><span> </span><span class="token known-class-name class-name">JSON</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">parse</span><span class="token" style="color:#a9b7c6">(</span><span>jsonString</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 88</span><span> </span><span class="token" style="color:#a9b7c6">}</span><span> </span><span class="token control-flow" style="color:#cc7832">catch</span><span> </span><span class="token" style="color:#a9b7c6">(</span><span>err</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#e8bf6a">any</span><span class="token" style="color:#a9b7c6">)</span><span> </span><span class="token" style="color:#a9b7c6">{</span><span> 89</span><span> ctx</span><span class="token" style="color:#a9b7c6">.</span><span class="token property-access">issues</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">push</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">{</span><span> 90</span><span> code</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#6a8759">"invalid_format"</span><span class="token" style="color:#a9b7c6">,</span><span> 91</span><span> format</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#6a8759">"json_string"</span><span class="token" style="color:#a9b7c6">,</span><span> 92</span><span> input</span><span class="token" style="color:#a9b7c6">:</span><span> jsonString</span><span class="token" style="color:#a9b7c6">,</span><span> 93</span><span> message</span><span class="token" style="color:#a9b7c6">:</span><span> err</span><span class="token" style="color:#a9b7c6">.</span><span class="token property-access">message</span><span class="token" style="color:#a9b7c6">,</span><span> 94</span><span> </span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 95</span><span> </span><span class="token control-flow" style="color:#cc7832">return</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token" style="color:#9876aa">NEVER</span><span class="token" style="color:#a9b7c6">;</span><span> 96</span><span> </span><span class="token" style="color:#a9b7c6">}</span><span> 97</span><span> </span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">,</span><span> 98</span><span> </span><span class="token function-variable" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">:</span><span> value </span><span class="token arrow" style="color:#a9b7c6">=></span><span> </span><span class="token known-class-name class-name">JSON</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">stringify</span><span class="token" style="color:#a9b7c6">(</span><span>value</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> 99</span><span></span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span></code></pre><p>
99You can pipe <code>jsonCodec</code> into other schemas for additional validation:</p><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> </span><span class="token maybe-class-name">UserFromJson</span><span> </span><span class="token" style="color:#a9b7c6">=</span><span> jsonCodec</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">pipe</span><span class="token" style="color:#a9b7c6">(</span><span>z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">object</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">{</span><span> 100</span><span> name</span><span class="token" style="color:#a9b7c6">:</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">string</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> 101</span><span> age</span><span class="token" style="color:#a9b7c6">:</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">number</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span> 102</span><span></span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 103</span> 104<span></span><span class="token maybe-class-name">UserFromJson</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">
104decode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">'{"name":"Alice","age":30}'</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// { name: "Alice", age: 30 }</span><span> 105</span><span></span><span class="token maybe-class-name">UserFromJson</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">{</span><span> name</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#6a8759">"Bob"</span><span class="token" style="color:#a9b7c6">,</span><span> age</span><span class="token" style="color:#a9b7c6">:</span><span> </span><span class="token" style="color:#6897bb">25</span><span> </span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// '{"name":"Bob","age":25}'</span></code></pre><h3><code>base64ToBytes</code></h3><pre style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;padding:1em;margin:.5em 0;overflow:auto;background:#2b2b2b"><code class="language-typescript" style="color:#a9b7c6;font-family:Consolas, Monaco, 'Andale Mono', monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none"><span class="token" style="color:#cc7832">const</span><span> base64ToBytes </span><span class="token" style="color:#a9b7c6">=</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">codec</span><span class="token" style="color:#a9b7c6">(</span><span>z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">base64</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">instanceof</span><span class="token" style="color:#a9b7c6">(</span><span class="token known-class-name class-name">Uint8Array</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> </span><span class="token" style="color:#a9b7c6">{</span><span> 106</span><span> </span><span class="token function-variable" style="color:#ffc66d">
106decode</span><span class="token" style="color:#a9b7c6">:</span><span> base64String </span><span class="token arrow" style="color:#a9b7c6">=></span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token property-access">core</span><span class="token" style="color:#a9b7c6">.</span><span class="token property-access">util</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">base64ToUint8Array</span><span class="token" style="color:#a9b7c6">(</span><span>base64String</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> 107</span><span> </span><span class="token function-variable" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">:</span><span> bytes </span><span class="token arrow" style="color:#a9b7c6">=></span><span> z</span><span class="token" style="color:#a9b7c6">.</span><span class="token property-access">core</span><span class="token" style="color:#a9b7c6">.</span><span class="token property-access">util</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">uint8ArrayToBase64</span><span class="token" style="color:#a9b7c6">(</span><span>bytes</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">,</span><span> 108</span><span></span><span class="token" style="color:#a9b7c6">}</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> 109</span> 110<span>base64ToBytes</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">
110decode</span><span class="token" style="color:#a9b7c6">(</span><span class="token" style="color:#6a8759">"SGVsbG8="</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// Uint8Array([72, 101, 108, 108, 111])</span><span> 111</span><span>base64ToBytes</span><span class="token" style="color:#a9b7c6">.</span><span class="token method property-access" style="color:#ffc66d">encode</span><span class="token" style="color:#a9b7c6">(</span><span>bytes</span><span class="token" style="color:#a9b7c6">)</span><span class="token" style="color:#a9b7c6">;</span><span> </span><span class="token" style="color:#808080">// "SGVsbG8="</span></code></pre><hr/><p>For further reading, see the <a href="https://github.com/colinhacks/zod/releases/tag/v4.1.0">Zod 4.1 release notes</a> and the <a href="https://zod.dev/codecs">Codecs documentation page</a>.</p></div></div></div></article><div class="mx-auto w-full max-w-5xl px-6 pt-24 sm:pt-28"><section id="writing" class="w-full"><h2 class="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Read on</h2><div class="mt-6 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3"><a href="/essays/ai-autodiscovery-in-package-json" class="group block"><article class="flex h-full flex-col overflow-hidden rounded-2xl border border-hairline bg-paper transition duration-[15ms] hover:border-coral/40 hover:shadow-[0_12px_30px_-12px_rgba(20,34,68,0.25)]"><div class="aspect-[16/9] overflow-hidden bg-paper-dim"><img src="/autodiscoverable-thumb.png" alt="" class="h-full w-full object-cover"/></div><div class="flex flex-1 flex-col gap-1 p-4"><h3 class="text-[16px] font-semibold leading-snug text-ink transition-colors duration-[15ms] group-hover:text-coral-deep">Making AI resources auto-discoverable via package.json</h3><time class="mt-auto pt-1 text-[12.5px] tabular-nums text-muted">Jul 2025</time></div></article></a><a href="/essays/live-types-typescript-monorepo" class="group block"><article class="flex h-full flex-col overflow-hidden rounded-2xl border border-hairline bg-paper transition duration-[15ms] hover:border-coral/40 hover:shadow-[0_12px_30px_-12px_rgba(20,34,68,0.25)]"><div class="aspect-[16/9] overflow-hidden bg-paper-dim"><img src="/live-typescript-thumb.png" alt="" class="h-full w-full object-cover"/></div><div class="flex flex-1 flex-col gap-1 p-4"><h3 class="text-[16px] font-semibold leading-snug text-ink transition-colors duration-[15ms] group-hover:text-coral-deep">Live types in a TypeScript monorepo</h3><time class="mt-auto pt-1 text-[12.5px] tabular-nums text-muted">May 2024</time></div></article></a><a href="/essays/reasonable-email-regex" class="group block"><article class="flex h-full flex-col overflow-hidden rounded-2xl border border-hairline bg-paper transition duration-[15ms] hover:border-coral/40 hover:shadow-[0_12px_30px_-12px_rgba(20,34,68,0.25)]"><div class="aspect-[16/9] overflow-hidden bg-paper-dim"><img src="/envelopes_small.jpg" alt="" class="h-full w-full object-cover"/></div><div class="flex flex-1 flex-col gap-1 p-4"><h3 class="text-[16px] font-semibold leading-snug text-ink transition-colors duration-[15ms] group-hover:text-coral-deep">An email regex for reasonable people</h3><time class="mt-auto pt-1 text-[12.5px] tabular-nums text-muted">Mar 2023</time></div></article></a></div></section></div><div class="h-28"></div></main><div style="flex:1" class="jsx-1725f8811803089d"></div><footer class="w-full border-t border-hairline"><div class="mx-auto flex w-full max-w-5xl items-center justify-between px-6 py-10"><p class="text-[13px] text-muted">© <!-- -->2026<!-- --> Colin McDonnell</p><nav class="flex items-center gap-4"><a href="https://twitter.com/colinhacks" aria-label="X / Twitter" class="text-muted transition-colors duration-[15ms] hover:text-coral-deep"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="h-[18px] w-[18px]"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24h-6.66l-5.214-6.817-5.967 6.817H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231 5.45-6.231Zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77Z"></path></svg></a><a href="https://github.com/colinhacks" aria-label="GitHub" class="text-muted transition-colors duration-[15ms] hover:text-coral-deep"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="h-[18px] w-[18px]"><path d="M12 .5C5.73.5.5 5.73.5 12.02c0 5.1 3.29 9.41 7.86 10.94.58.11.79-.25.79-.56 0-.27-.01-1-.02-1.96-3.2.7-3.88-1.54-3.88-1.54-.53-1.35-1.29-1.71-1.29-1.71-1.05-.72.08-.71.08-.71 1.16.08 1.77 1.2 1.77 1.2 1.03 1.7
1117 2.7 1.26 3.36.96.1-.75.4-1.26.73-1.55-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18.92-.26 1.91-.39 2.89-.39.98 0 1.97.13 2.89.39 2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.12 3.05.74.8 1.18 1.83 1.18 3.09 0 4.42-2.69 5.39-5.25 5.68.41.36.78 1.06.78 2.14 0 1.55-.01 2.8-.01 3.18 0 .31.21.68.8.56A11.53 11.53 0 0 0 23.5 12.02C23.5 5.73 18.27.5 12 .5Z"></path></svg></a><a href="/rss.xml" aria-label="RSS feed" class="text-muted transition-colors duration-[15ms] hover:text-coral-deep"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="h-[18px] w-[18px]"><path d="M6.18 17.82a2.18 2.18 0 1 1-4.36 0 2.18 2.18 0 0 1 4.36 0ZM2 9.86v3.04A7.1 7.1 0 0 1 9.1 20h3.04A10.14 10.14 0 0 0 2 9.86ZM2 3.84v3.04c7.25 0 13.12 5.87 13.12 13.12h3.04C18.16 11.1 11.06 3.84 2 3.84Z"></path></svg></a></nav></div></footer></div></div>
111<script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"post":{"rssId":"/essays/introducing-zod-codecs","relativeUrl":"/essays/introducing-zod-codecs","url":"https://colinhacks.com/essays/introducing-zod-codecs","title":"Introducing Zod Codecs","subtitle":null,"published":true,"description":null,"canonicalUrl":"https://colinhacks.com/essays/introducing-zod-codecs","publishedAt":1755909868170,"updatedAt":null,"tags":["Zod","TypeScript","Validation"],"author":"Colin McDonnell","authorHandle":"colinhacks","authorImage":"https://colinhacks.com/headshot_small.jpg","bannerImage":"https://colinhacks.com/codec-neon.png","thumbImage":"https://colinhacks.com/codec-neon-small.png","content":"\nZod 4.1 introduced a new `z.codec()` API for defining bi-directional transformations in Zod.\n\n## The problem with transforms\n\nZod's `.transform()` method is great for one-way data conversion:\n\n```ts\nconst stringToNumber = z.string().transform(val =\u003e parseFloat(val));\nstringToNumber.parse(\"42\"); // 42\n```\n\nBut what if you need to go both ways? Say, you're storing dates as ISO strings in a database but want to work with `Date` objects in your app.\n\n```ts\nconst stringToDate = z.string().transform(str =\u003e new Date(str));\nconst dateToString = z.date().transform(date =\u003e date.toISOString());\n\n// Two separate schemas, manually kept in sync\nstringToDate.parse(\"2024-01-15T10:30:00.000Z\"); // Date\ndateToString.parse(new Date()); // \"2024-01-15T10:30:00.000Z\"\n```\n\nThis works, but it's brittle. You need to keep track of two schemas and remember that they are intended as inverses. You need to manually verify that the output type of one matches the input type of the other. If you change one, you have to remember to update the other. \n\n## Introducing codecs\n\nCodecs are a new Zod API for defining *bidirectional transformations* between two types. You specify an input schema, output schema, and transformation functions in both directions:\n\n```ts\nconst stringToDate = z.codec(\n z.iso.datetime(), // input schema: ISO string\n z.date(), // output schema: Date object\n {\n decode: isoString =\u003e new Date(isoString), // string â Date\n encode: date =\u003e date.toISOString(), // Date â string\n }\n);\n```\n\nYou can process data in both directions using the new top-level `.decode()` and `.encode()` methods:\n\n```ts\nstringToDate.decode(\"2024-01-15T10:30:00.000Z\");
111 // Date\nstringToDate.encode(new Date(\"2024-01-15\")); // \"2024-01-15T00:00:00.000Z\"\n```\n\n\u003e **Note** â For bundle size reasons, these new methods have not added to Zod Mini schemas. Instead, this functionality is available via equivalent top-level functions. \n\u003e\n\u003e ```ts\n\u003e // equivalent at runtime\n\u003e z.decode(stringToDate, \"2024-01-15T10:30:00.000Z\");\n\u003e z.encode(stringToDate, new Date());\n\u003e ```\n\n\nThis is particularly important when you are using Zod to *map data* back and forth between two different domains. One common use case is to convert data to/from a serializable format like JSON into a richer JavaScript representation (with `Date`, `bigint`, etc).\n\n\n\n### Async \n\nThe transformation functions can be `async`.\n\n```ts\nconst asyncCodec = z.codec(z.string(), z.number(), {\n decode: async str =\u003e Number(str),\n encode: async num =\u003e num.toString(),\n});\n```\n\nThe usual \"safe\" and \"async\" variants exist:\n\n```ts\nsyncCodec.encode(\"42\");\nsyncCodec.safeEncode(\"42\");\nawait asyncCodec.encodeAsync(\"42\");\nawait asyncCodec.safeEncodeAsync(\"42\");\n```\n\n\n### Composability\n\nCodecs can be composed inside other schemas, just like any other schema. There are no special rules.\n\n```ts\nconst queryParams = z.object({\n before: stringToDate,\n after: stringToDate\n})\n\nqueryParams.encode({\n before: new Date(),\n after: new Date()\n});\n// =\u003e { before: string, after: string }\n```\n\n\n\n### `.parse()` vs `.decode()`\n\nLet's compare the existing `.parse()` APIs to `.decode()`. _`.parse()` is equivalent to `.decode()` at runtime._\n\n```ts\n// equivalent at runtime\nstringToDate.parse(\"2024-01-15T10:30:00.000Z\"); \nstringToDate.decode(\"2024-01-15T10:30:00.000Z\");\n```\n\nThough they're identical at runtime, their type signatures differ in an important way. While `.parse()` accepts `unknown`, `decode` expects a _strongly-typed inputs_.\n\n```ts\nstringToDate.parse(12345); \n// No TypeScript error but fails at runtime\n\nstringToDate.decode(12345);\n// â TypeScript error: Argument of type 'number' is not assignable to parameter of type 'string'\n```\n\nHere's a diagram demonstrating the differences:\n\n\n\n\u003e This is a highly requested feature unto itself.\n\u003e \n\u003e - [#3860](https://github.com/colinhacks/zod/issues/3860) Add strongly typed parse function\n\u003e - [#1748](https://github.com/colinhacks/zod/issues/1748) Typed input for parse methods\n\u003e - [#3978](https://github.com/colinhacks/zod/issues/3978) Type-safe parsing with known input types\n\u003e - [#1892](https://github.com/colinhacks/zod/issues/1892) Strongly typed decode function\n\n## How encoding works\n\nMost Zod schemas in the universe don't perform any kind of transformation. Their inferred input and output types are identical. For these schemas, there is no difference between parsing/decoding and encoding.\n\n```ts\nconst mySchema = z.object({\n name: z.string()\n});\n\n// no difference\nmySchema.parse({ name: \"colinhacks\" });\nmySchema.decode({ name: \"colinhacks\" })\nmySchema.encode({ name: \"colinhacks\" })\n```\n\nA small number of APIs cause the input and output types to diverge. In these scenarios, the runtime behavior of `.decode()`/`.encode()` also differ.\n\n### Codecs\n\nThis is an obvious one. During `.decode()`, the `decode` function runs. During `.encode()`, the `encode` function runs. Simple.\n\n### Transforms â ï¸\n\nThis is the #1 rule of `.encode()`: you can't use `.transform()`. That API is inherently unidirectional. If your schema contains any transforms, attempting an \"encode\" operation with it will throw a runtime error. You'll need to refactor to use `z.codec()`.\n\n```ts\nconst schema = z.string().transform(val =\u003e val.length);\n\nschema.encode(5); \n// â ZodEncodeError: Encountered unidirectional transform during encode\n```\n\n### Pipes\n\n\u003e **Note** â Codecs are actually implemented as a subclass of `ZodPipe` augmented with \"interstitial\" transform logic.\n\nPipes reverse their order during encoding, from `A â B` to `B â A`. That said, pipes are typically used in conjunction with transforms, so \"vanilla\" pipes are rarely useful in the context of encoding. Prefer `z.codec()` everywhere.\n\n### Refinements\n\nAll checks (`.refine()`, `.min()`, `.max()`, etc.) are still executed in both directions. \
111n\n```ts\nconst schema = stringToDate.refine((date) =\u003e date.getFullYear() \u003e 2000, \"Must be this millenium\");\n\nschema.encode(new Date(\"2000-01-01\"));\n// =\u003e Date\n\nschema.encode(new Date(\"1999-01-01\"));\n// =\u003e â ZodError: [\n// {\n// \"code\": \"custom\",\n// \"path\": [],\n// \"message\": \"Must be this millenium\"\n// }\n// ]\n```\n\nTo avoid unexpected errors in your custom `.refine()` logic, Zod performs two \"passes\" during `.encode()`. The first pass ensures the input type conforms to the expected type (no `invalid_type` errors). If that passes, Zod performs the second pass which executes the refinement logic.\n\nThis approach means all parsing \u0026 refinement logic runs in exactly the reverse order during encoding. Even \"mutating refinements\" like `z.string().trim()` or `z.string().toLowerCase()` work as expected. \n\n```ts\nconst schema = z.string().trim();\n\nschema.decode(\" hello \");\n// =\u003e \"hello\"\n\nschema.encode(\" hello \");\n// =\u003e \"hello\"\n```\n\n### Default/prefault\n\nDefault and prefault values are only applied in the forward direction. \n\n```ts\nconst withDefault = z.string().default(\"hello\");\n\nwithDefault.decode(undefined); // \"hello\"\nwithDefault.encode(undefined); // â ZodError\n```\n\nThis is by design. When you add a default, the input becomes `string | undefined` but the output stays `string`. As such, `undefined` isn't considered a valid input to `.encode()`.\n\n### Catch\n\nSimilarly, `.catch()` values are only applied in the forward direction.\n\n### Stringbool\n\n\u003e **Note** â [Stringbool](https://zod.dev/api?id=stringbool) pre-dates the introduction of codecs in Zod. It has since been internally re-implemented as a codec. \n\nThe `z.stringbool()` API converts string values (`\"true\"`, `\"false\"`, `\"yes\"`, `\"no\"`, etc.) into `boolean`. By default, it will convert `true` to `\"true\"` and `false` to `\"false\"` during `.encode()`..\n\n```ts\nconst stringbool = z.stringbool();\n\nstringbool.decode(\"true\"); // =\u003e true\nstringbool.decode(\"false\"); // =\u003e false\n\nstringbool.encode(true); // =\u003e \"true\"\nstringbool.encode(false); // =\u003e \"false\"\n```\n\nIf you specify a custom set of `truthy` and `falsy` values, the *first element in the array* will be used instead.\n\n```ts\nconst stringbool = z.stringbool({ truthy: [\"yes\", \"y\"], falsy: [\"no\", \"n\"] });\n\nstringbool.encode(true); // =\u003e \"yes\"\nstringbool.encode(false); // =\u003e \"no\"\n```\n\n\n## Official codecs\n\nZod doesn't provide any predefined codecs out of the box. Instead, the docs provide some \"canonical\" codec implementations you can copy/paste into your projects as needed. These have all been tested internally. \n\n- [`stringToNumber`](https://zod.dev/codecs?id=stringtonumber)\n- [`stringToInt`](https://zod.dev/codecs?id=stringtoint)\n- [`stringToBigInt`](https://zod.dev/codecs?id=stringtobigint)\n- [`numberToBigInt`](https://zod.dev/codecs?id=numbertobigint)\n- [`isoDatetimeToDate`](https://zod.dev/codecs?id=isodatetimetodate)\n- [`epochSecondsToDate`](https://zod.dev/codecs?id=epochsecondstodate)\n- [`epochMillisToDate`](https://zod.dev/codecs?id=epochmillistodate)\n- [`jsonCodec`](https://zod.dev/codecs?id=jsoncodec)\n- [`utf8ToBytes`](https://zod.dev/codecs?id=utf8tobytes)\n- [`bytesToUtf8`](https://zod.dev/codecs?id=bytestoutf8)\n- [`base64ToBytes`](https://zod.dev/codecs?id=base64tobytes)\n- [`base64urlToBytes`](https://zod.dev/codecs?id=base64urltobytes)\n- [`hexToBytes`](https://zod.dev/codecs?id=hextobytes)\n- [`stringToURL`](https://zod.dev/codecs?id=stringtourl)\n- [`stringToHttpURL`](https://zod.dev/codecs?id=stringtohttpurl)\n- [`uriComponent`](https://zod.dev/codecs?id=uricomponent)\n- [`stringToBoolean`](https://zod.dev/codecs?id=stringtoboolean)\n\nSome selected examples are below.\n\n### `stringToBigInt`\n\n```ts\nconst stringToBigInt = z.codec(z.string(), z.bigint(), {\n decode: str =\u003e BigInt(str),\n encode: bigint =\u003e bigint.toString(),\n});\n\nstringToBigInt.decode(\"12345\"); // 12345n\nstringToBigInt.encode(12345n); // \"12345\"\n```\n\n### `jsonCodec`\n\n```ts\nconst jsonCodec = z.codec(z.string(), z.json(), {\n decode: (jsonString, ctx) =\u003e {\n try {\n return JSON.parse(jsonString);\n } catch (err: any) {\n ctx.issues.push({\n code: \"invali
111d_format\",\n format: \"json_string\",\n input: jsonString,\n message: err.message,\n });\n return z.NEVER;\n }\n },\n encode: value =\u003e JSON.stringify(value),\n});\n```\n\nYou can pipe `jsonCodec` into other schemas for additional validation:\n\n```ts\nconst UserFromJson = jsonCodec.pipe(z.object({ \n name: z.string(), \n age: z.number() \n}));\n\nUserFromJson.decode('{\"name\":\"Alice\",\"age\":30}'); // { name: \"Alice\", age: 30 }\nUserFromJson.encode({ name: \"Bob\", age: 25 }); // '{\"name\":\"Bob\",\"age\":25}'\n```\n\n### `base64ToBytes`\n\n```ts\nconst base64ToBytes = z.codec(z.base64(), z.instanceof(Uint8Array), {\n decode: base64String =\u003e z.core.util.base64ToUint8Array(base64String),\n encode: bytes =\u003e z.core.util.uint8ArrayToBase64(bytes),\n});\n\nbase64ToBytes.decode(\"SGVsbG8=\"); // Uint8Array([72, 101, 108, 108, 111])\nbase64ToBytes.encode(bytes); // \"SGVsbG8=\"\n```\n\n---\n\nFor further reading, see the [Zod 4.1 release notes](https://github.com/colinhacks/zod/releases/tag/v4.1.0) and the [Codecs documentation page](https://zod.dev/codecs)."},"posts":[{"rssId":"/essays/ai-autodiscovery-package.json","relativeUrl":"/essays/ai-autodiscovery-in-package-json","url":"https://colinhacks.com/essays/ai-autodiscovery-in-package-json","title":"Making AI resources auto-discoverable via package.json","subtitle":null,"published":true,"description":null,"canonicalUrl":"https://colinhacks.com/essays/ai-autodiscovery-in-package-json","publishedAt":1753838584424,"updatedAt":null,"tags":["TypeScript","AI","Agents"],"author":"Colin McDonnell","authorHandle":"colinhacks","authorImage":"https://colinhacks.com/headshot_small.jpg","bannerImage":"https://colinhacks.com/autodiscoverable.png","thumbImage":"https://colinhacks.com/autodiscoverable-thumb.png","content":"\n\nI propose the following convention for JavaScript libraries to make their AI resources auto-discoverable via package.json.\n\n```json\n{\n // package.json\n \"llms\": \"https://docs.cool-library.com/llms.txt\",\n \"llmsFull\": \"https://docs.cool-library.com/llms-full.txt\",\n \"mcpServer\": \"https://mcp.cool-library.com\",\n}\n```\n\nI've implemented this in [Zod](https://github.com/colinhacks/zod/blob/main/packages/zod/package.json) and encourage my fellow library authors to do the same. Moreover, I encourage agentic IDEs/CLIs to implement first-party support for this convention. \n\nIf you add support to your library or agent, please fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSew8sx6KxjwHkBGlgbUWHNEzSAt9GM8ryJ-X39d0gEltGT_-g/viewform?usp=header).\n\n## Details\n\n1. The `llms` field is a URL pointing to an [llms.txt](https://llmstxt.org/) file. If you aren't familiar, this is a proposed convention for documentation sites to provide an AI-friendly \"sitemap\" to AI agents. It's a simple spec: basically just a bunch of categorized Markdown links.\n2. The `llmsFull` field is a URL pointing to an `llms-full.txt` file containing the full documentation for the library. Typically this is a concatenation of all the `.md`/`.mdx` documentation pages in the docs.\n3. The `mcpServer` field is a URL pointing to an MCP server. For a typical library, this server would offer a simple Q\u0026A tool that is capable of answering questions about the library's API/usage. This can use RAG over the library's docs, GitHub issues, Discord server, etc.\n\n## Do agents already look at these fields?\n\nNo. This is a proposed convention. As more libraries implement this, there is more incentive for agents (IDEs, CLIs, etc) to implement first-party support. On the flip side of the coin, if any agentic CLI/IDE implements support, there is more incentive for libraries to implement this convention. There's a chicken-and-egg problem.\n\nFortunately, you can probably get your agent of choice to support this with some custom instructions in your `AGENTS.md` file.\n\n```md\n\u003c!-- AGENTS.md --\u003e\nFor each package in my package.json, read its package.json and look for the `llms`, `llmsFull`, and `mcpServer` fields. If found, use these resources to work with that library:\n\n- `llms`: Use this URL to fetch the llms.txt file, which contains categorized Markdown links to the library's documentation. Use this as a sitemap to understand what documentation is available and how it's organized.\n\n- `llmsFull`: Use this URL to fetch the llms-full.txt file, which contains the complete documentation for the library. Use this for c
111omprehensive understanding of the library's API, features, and usage patterns.\n\n- `mcpServer`: Connect to this MCP server URL to access specialized tools for answering questions about the library. Use this server's Q\u0026A capabilities when I need specific help with the library's API, troubleshooting, or best practices. If possible, prompt the user to add this server to their MCP server registry.\n```\n\n## Why not `AGENTS.md`?\n\nThis doesn't solve the same problem.\n\n`AGENTS.md` is for telling agents how to work *inside* your codebase. It's like a contributor's guide: how to run tests, run builds, lint, update configs, etc. It gives the agent best practices for modifying a codebase.\n\nThis is a qualitatively different problem that a library trying to advertise it's own functionality to an agent. The codebase is already installed in `node_modules`; agents shouldn't be modifying it. \n\nAn agent doesn't care about the package's internals, it only cares about the public API. Like any developer, it doesn't want to read your code to learn how to do stuffâthat's hard, time-consuming, and expensive. Having some well-known `package.json` fields that have an established semantic meaning is a cleaner solution with higher signal-to-noise ratio.\n\n---\n\nIf you maintain a library that implements this convention, fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSew8sx6KxjwHkBGlgbUWHNEzSAt9GM8ryJ-X39d0gEltGT_-g/viewform?usp=header).\n\n\n\n\u003c!-- \n\n\n1. this is a proposed convention. no agents read this fields currently. to my maintainer friends: I encourage you to do the same. if enough libraries do this, the agent CLIs/IDEs will eventually implement support\n\n2. \"why not AGENTS.md?\" this doesn't solve the same problem. agents.md is used in a codebase to tell agents the structure of your codebase, how to run common tasks (test running, linting, etc). it's a \"contributor's guide\" for agents. this is a qualitative different problem than a library trying to advertise its own functionality to an agent. the structure of the codebase doesn't matter. agents don't care. they just want to know the consumable API: how to use the library.\n\n3. \"why would libraries have an MCP server anyway?\" in Zod's case, all of its docs and issues are all RAG-ified by Inkeep and queryable via the MCP server. \nhttps://x.com/colinhacks/status/1950109840560238644 --\u003e"},{"rssId":"/essays/live-types-typescript-monorepo","relativeUrl":"/essays/live-types-typescript-monorepo","url":"https://colinhacks.com/essays/live-types-typescript-monorepo","title":"Live types in a TypeScript monorepo","subtitle":null,"published":true,"description":null,"canonicalUrl":"https://colinhacks.com/essays/live-types-typescript-monorepo","publishedAt":1717106344264,"updatedAt":null,"tags":["TypeScript","Monorepos"],"author":"Colin McDonnell","authorHandle":"colinhacks","authorImage":"https://colinhacks.com/headshot_small.jpg","bannerImage":"https://colinhacks.com/live-typescript.png","thumbImage":"https://colinhacks.com/live-typescript-thumb.png","content":"\n\u003e EDIT: A previous version of this post recommended `publishConfig`, operating under the mistaken belief that it could be used to override `\"exports\"` during `npm publish`. As it turns out, `npm` only uses `\"publishConfig\"` to override certain `.npmrc` fields like `registry` and `tag`, whereas `pnpm` has expanded its use to override package metadata like `\"main\"`, `\"types\"`, and `\"exports\"`. There are a number of reasons you may not wish to strongly couple your deployment logic to `pnpm` (detailed in the `publishConfig` section below). My updated recommendation is to use a custom export condition plus `customConditions` in `tsconfig.json`.\n\n\u003e Jump into the code: https://github.com/colinhacks/live-typescript-monorepo\n\nIn development, your TypeScript code should feel \"alive\". When you update your code in one file, the effects of that change should propagate to all files that import it instantaneously, with no build step. This is true even for monorepos, where you may
111not be importing things from a file, but from a local package.\n\n```diff\n- import { Fish } from \"../pkg-a/index\";\n+ import { Fish } from \"pkg-a\"\n```\n\nThis is vital to TypeScript's value proposition.\n\nThis post explains a few strategies you can use to make your TypeScript monorepo feel more alive. Refer to the corresponding repo where you can play around with the code for each strategy: https://github.com/colinhacks/live-typescript-monorepo\n\nThe repo contains three subdirectories, each containing a monorepo with the following file structure:\n\n```\n.\nâââ package.json\nâââ packages\nâ  âââ pkg-a\nâ  â  âââ README.md\nâ  â  âââ index.ts\nâ  â  âââ package.json\nâ  â  âââ tsconfig.json\nâ  âââ pkg-b\nâ  âââ README.md\nâ  âââ index.ts\nâ  âââ package.json\nâ  âââ tsconfig.json\nâââ pnpm-lock.yaml\nâââ pnpm-workspace.yaml\nâââ tsconfig.base.json\nâââ tsconfig.json\n```\n\nThis is a pnpm monorepo (`pnpm-workspace.yaml`) with two packages, `pkg-a` and `pkg-b`. **`pkg-b` has a dependency on `pkg-a`**. Each package has a `tsconfig.json` that extends a `tsconfig.base.json` in the root of the monorepo.\n\nHere is a quick rundown of the solutions. Don't worry if there are terms you're not familiar with, everything is explained in the breakdown.\n\n1. Use project references (`\"references\"` in `tsconfig.json`)\n2. Use `publishConfig` in `package.json` to specify `.ts` file in development and `.js` file in production. _Requires `pnpm`._\n3. Configure `compilerOptions.paths` in `tsconfig.json` to override resolution for local package names.\n4. **Recommended** Define a custom conditional export condition in `package.json#exports`.\n\nNote that I'm explaining all the solutions I found for the sake of education, but the recommended solution is #5 (custom export conditions) so feel free to jump ahead if you're just looking for a solution!\n\n## A primer: runtime vs. static\n\nThe fundamental annoyance here is this: Node.js has an algorithm for _module resolution_. When it sees an import from a _bare specifier_ like `\"pkg-a\"`, it scans up the directory tree checking for a directory called `\"pkg-a\"` in each `node_modules` folder it encounters. Once it finds `\"pkg-a\"`, it reads the `package.json` inside and uses the `main` and `exports` to figure out how to resolve the bare specifier to a file on disk.\n\nThe TypeScript server does _almost_ the same thing, though it uses the `\"types\"` field (or the `\"types\"` export condition in `\"exports\"`) to find the _declaration file_ corresponding to a particular package. There are also _lots_ of ways to hack TypeScript's module resolution algorithm. (We'll get to that in a bit.)\n\nBut in development, we want things to behave differently. We want the TypeScript server to look at our \"raw\" `.ts` files when resolving imports to other local packages in our monorepo, _not_ the compiled declaration files. Similarly, when we execute our local code (say, when running tests) we want it to \"run\" our TypeScript source code. You shouldn't need to rebuild your project before running tests.\n\nSo we need a way to hijack module resolution both statically (for TypeScript) and at runtime (for Node.js or tools like Vitest). We also need to make sure _both_ of these things are hijacked in a way that they agree with each other. If TypeScript is looking at our `src/index.ts` files but Node.js is still importing `lib/index.js`, the types may not reflect the runtime behavior of the code. You may have seen this referred to as _static-runtime disagreement_.\n\nOkay, enough background. Let's get into the details.\n\n## 1. Project references\n\n\u003e The code for this is under the `project-references` subdirectory.\n\u003e Project references are a TypeScript feature that make it easier to split a large TypeScript codebase into chunks that are typechecked separately by the TypeScript server. This can be a huge performance win for large codebase
111s.\n\nIn our monorepo, `pkg-b` has a dependency on `pkg-a`. So in our `packages/pkg-b/tsconfig.json`, we can add a reference to `pkg-a` like so:\n\n```json\n{\n \"references\": [{\"path\": \"../pkg-a\"}]\n}\n```\n\nUnfortunately the `\"references\"` field is [not inherited](https://twitter.com/andhaveaniceday/status/1798613232208527826) when you use `\"extends\"`, so you have to declare it in every package's `tsconfig.json`. If your monorepo packages have a lot of interconnection, this can get unwieldy fast.\n\nYou may also notice that `\"references\"` will end up [mirroring the `\"dependencies\"`](https://twitter.com/andhaveaniceday/status/1798717319990112609) list in `package.json`. These will need to be kept in sync to work as expected. There are tools ([`nx`](https://nx.dev/)) that try to automate this, but the need to run a command to re-generate configs starts to feel like a \"build step\" in itself...and that's what we're trying to avoid.\n\n\u003e There are [long-gestating efforts](https://github.com/microsoft/TypeScript/issues/25376) to make this more ergonomic, but there seems to be little movement on this front.\n\nThe final nail in the coffin is the difficulty of incorporating `\"references\"` into runtime module resolution. It's purely a TypeScript feature, and no tools incorporate this into their module resolution. So in all likelihood, you'd need to use one of the approaches below _in conjunction_ with project references to achieve true \"live types\" with runtime `.ts` resolution.\n\n\u003e There are absolutely scenarios where project references are indispensable. If you have a large enough monorepo, project references may become necessary to avoid re-typechecking the entire codebase when you make a change! But for non-giant projects, they aren't necessary and introduce too much complexity and potential footguns.\n\n## 2. `\"publishConfig\"` in `package.json`\n\n\u003e This approach requires `pnpm` to work! The `publishConfig` field behaves very differently between `npm publish` and `pnpm publish`.\n\nOne obvious solution is just to have each package's `package.json` point to our `.ts` files in `package.json`.\n\n```json\n{\n \"name\": \"pkg-a\",\n \"main\": \"./src/index.ts\",\n \"types\": \"./src/index.ts\",\n \"exports\": {\n \".\": {\n \"import\": \"./src/index.ts\",\n \"require\": \"./src/index.ts\",\n \"types\": \"./src/index.ts\"\n },\n \"./package.json\": \"./package.json\"\n }\n}\n```\n\nThis introduces an obvious and immediate problem. We can't publish our raw `.ts` files to npm without breaking most tools. They need to be properly transpiled to JavaScript first. When we run `npm publish`, we need these fields to point to the appropriate `lib/index.js` and `lib/index.d.ts` files.\n\nMany people have written custom build scripts that will duplicate `package.json` and rewrite these fields before publishing. Fortunately the good folks at `pnpm` have given us a better option: `publishConfig`.\n\n```json\n{\n \"name\": \"pkg-a\",\n\n // development config\n \"exports\": \"./src/index.ts\",\n\n // production config\n \"publishConfig\": {\n \"main\": \"./lib/index.js\",\n \"types\": \"./lib/index.d.ts\",\n \"exports\": {\n \".\": {\n \"import\": \"./lib/index.js\",\n \"require\": \"./lib/index.js\",\n \"types\": \"./lib/index.d.ts\"\n },\n \"./package.json\": \"./package.json\"\n }\n }\n}\n```\n\nWhen you run `pnpm publish`, pnpm will read the `publishConfig` field in `package.json` and use those values to override the top-level values for `\"main\"`, `\"exports\"`, and `\"types\"`.\n\n\u003e While `npm` supports a field called `publishConfig`, it only lets you set `npm config` settings like `registry` and `tag`. Sad.\n\nIn essence, our top-level `exports`, `main`, `types`, etc. now _only apply in development_, so we can point these to raw `.ts` files! TypeScript will happily parse these files, as will any modern bundler, framework, or a tool like `tsx`.\n\nFor simplicity, I've only set one field: `exports`, and I'm setting it to a simple string value. This has some benef
111its:\n\n1. It's clean! You don't need to redundantly declare a big `\"exports\"` object in both the top-level `package.json` and `\"publishConfig\"`. (Though you still can if you rely on subpath imports.)\n2. It's safe! By specifying `\"exports\"` as a single string, no subpath imports are allowed. That level of strictness is good. It means you can't use subpath imports internally that would be illegal using the configuration specified in `publishConfig`.\n3. It just works at runtime. Tools like `tsx`, `esbuild`, Vite, etc. can all happily resolve this import using just the single `exports` key. You don't need `\"main\"` unless you're using Node.js 10 or earlier in your development environment.\n4. Similarly, TypeScript is happy. I lied a bit earlier...it doesn't need a special `\"types\"` field. It's more than happy to fall back to `\"exports\"` and pull type signatures out of a regular `.ts` source file.\n\nThe big downside is the reliance on `pnpm`. That means if you ever run `npm publish` by accident, you'll accidentally publish a package that isn't runnable by Node.js ð You also can't rely on popular GitHub Actions like `JS-DevTools/npm-publish` since those use `npm`. This obstacle is surmountable with some tweaks to CI and diligence around publishing, but it is an important gotcha.\n\n## 3. `\"paths\"` in `tsconfig.json`\n\nTypeScript provides another way to \"hijack\" module resolution: the `compilerOptions.paths`.\n\n```json\n{\n \"compilerOptions\": {\n \"paths\": {\n \"pkg-a\": [\"./packages/pkg-a/src/index.ts\"],\n \"pkg-b\": [\"./packages/pkg-b/src/index.ts\"]\n }\n }\n}\n```\n\nThe `compilerOptions.paths` option overrides TypeScript's normal module resolution. Any import that matches a key in paths will be resolved to the corresponding file. (If you specify multiple paths, TypeScript will use the first one that exists on your file system.)\n\nThis should be added to the `tsconfig.json` for every package in your monorepo. You can avoid redundancy by having all of your package `tsconfig`s extend a shared `tsconfig.base.json`.\n\nRemember, we also need to incorporate this into our runtime module resolution.\n\nThe popular `tsx` tool by [`@privatenumbr`](https://twitter.com/privatenumbr) [does this automatically](https://tsx.is/faq#how-does-tsx-compare-to-ts-node). This is the best way to run a TypeScript file with Node.js.\n\n```sh\n$ npm install -g tsx\n$ tsx src/index.ts\n```\n\nYou can also use `tsx` in conjunction with Node.js's `--import` flag.\n\n```sh\n$ npm install tsx\n$ node --import tsx src/index.ts\n```\n\nIn the Vite/Vitest ecosystem, there is a popular plugin to do the same called [`vite-tsconfig-paths`](https://www.npmjs.com/package/vite-tsconfig-paths).\n\nThis solution can be a bit fiddly, and requires some diligence in how you configure per-package `tsconfigs`. It should also be noted that the TypeScript team [kinda hates](https://twitter.com/andhaveaniceday/status/1770466153615577306) `tsconfig.paths`, and there are a lot of ways to shoot yourself in the foot with it.\n\n## 4. `liveDev` mode in `tshy`\n\nThe [`tshy`](https://github.com/isaacs/tshy) (**T**ype**S**cript **Hy**bridizer) tool is an opinionated tool by the creator of `npm` that makes it simple to build ESM and CommonJS packages from your TypeScript source code.\n\nIt recently added support for a `liveDev` mode that will hardlink your TypeScript source code into `./dist/esm` and `./dist/commonjs` directories. To set this up, install `tshy` into `devDependencies`.\n\n```sh\n$ pnpm add tshy --dev\n```\n\nAdd the following `\"tshy\"` config to your `package.json`:\n\n```json\n{\n \"tshy\": {\n \"liveDev\": true\n }\n}\n```\n\nThen run `tshy` in your package directory.\n\n```sh\n$ npx tshy\n```\n\n\u003e For simplicity, add `\"tshy\"` as the `\"build\"` script in each of your workspaces. Then you can run this script in each workspace with one command from the root of your workspace. (The exact command depends on your package manager.)\n\nThis lets VS Code discover your live TypeScript source code without any additional `package.json` configuration! And tools like TypeScript and Vitest will be able to resolve your workspace imports to the `.ts` files with no additional configuration at all!\n\nThe downside is that this requires running `tshy` in each of your workspaces packages, which starts to feel like a build step.\n\nThe upside is that you only need to do this once! Once your `src` files are hard-linked into `dist`, you can edit them like normal and those changes are automatically reflected in `dist`. (This is how hard links work.)\n\n\u003e The downside is that you'll need to re-run `tshy` each time you add a new TypeScript file (due to how hard links work...). Running `tshy --watch` can mitigate the \"new file\" problem, but for the purposes of this repo, I'm avoiding any solutions that require a file system watcher.\n\n## 5. Custom conditions in `\"exports\"`\n\nThis lets you specify your TypeScript files in `package.json#exports` under a custom _[export condition](https://nodejs.org/api/packages.html#conditional-exports)_ of your choosing.\n\nThe mostly widely-utilized export conditions are `import` (for specifying ESM code), `require` (for specifying CJS code), and `types` (for specifying type definitions).\n\n```jsonc\n// package.json\n{\n \"name\": \"pkg-a\",\n \"exports\": {\n \"*\": {\n \"import\": \"./lib/index.js\",\n \"require\": \"./lib/index.cjs\"\n }\n }\n}\n```\n\nBut you're allowed to use any string you like as a custom export condition! Export conditions are intended as an open-ended mechanism for users to specify import entrypoints for specific runtimes, bundlers, or other tooling. For instance, `\"deno\"`, `\"bun\"`, and `\"workerd\"` (Cloudflare Workers) all support custom conditions so libraries can ship build that are specific to those runtimes.\n\nHere's how a custom condition might look in your `package.json`. There's nothing special about the string `\"@colinhacks/zod\"` here! It could be anything.\n\n```jsonc\n// package.json\n{\n \"name\": \"pkg-a\",\n \"exports\": {\n \"*\": {\n \"import\": {\n \"@colinhacks/source\": \"./src/index.ts\", // must be first, order matters!\n \"default\": \"./lib/index.js\",\n \"types\": \"./lib/index.d.ts\"\n },\n \"require\": {\n \"@colinhacks/source\": \"./src/index.ts\",\n \"default\": \"./lib/index.cjs\",\n \"types\": \"./lib/index.d.ts\"\n }\n }\n }\n}\n```\n\n\u003e You should put your custom condition _first_. Order matters! It's important that your custom condition is the first one in the list (even before `\"types\"`).\n\nBy default, neither TypeScript nor Node.js pays attention to this new `\"@colinhacks/source\"` export condition. You need to tell them to incorporate it into their respective module resolution algorithms.\n\nTo tell TypeScript, add `\"@colinhacks/source\"` to the `customConditions` field in `tsconfig.json` for all packages in your monorepo. (You can avoid redundancy by having all of your package `tsconfig`s extend a shared `tsconfig.base.json`.)\n\n```json\n{\n \"compilerOptions\": {\n // include this in the tsconfig for all packages\n \"customConditions\": [\"@colinhacks/source\"]\n }\n}\n```\n\nNode.js can accept custom conditions via the `--conditions` flag.\n\n```sh\nnode --import=tsx --conditions=@colinh
111acks/source ./src/index.ts\n```\n\nIn the Vite/Vitest ecosystem, this can be configured with `resolve.conditions` setting.\n\n```js\n// vite.config.json\nexport default {\n resolve: {\n conditions: ['@colinhacks/source'],\n },\n};\n```\n\n### A note on condition naming\n\nI chose `\"@colinhacks/source\"` as the condition name for the sake of uniqueness. You want to pick a condition name that will only be defined in your workspace packages _only_. If you choose something generic like `\"source\"`, you may have dependencies with the same condition defined in their `package.json`. In this case, VS Code will resolve imports of that package using its `\"source\"` files, instead of using it's pre-compiled `.d.ts` files from `\"types\"`. This can hurt performance of the TypeScript server and slow down your editor.\n\n## Conclusion\n\nOverall, my recommendation is #5: custom export conditions.\n\n- It's clean, easy to configure, and works well with modern tooling.\n- It doesn't require you to use `pnpm`, which is a non-starter for many projects.\n- You don't need to worry about keeping runtime and TypeScript configurations in sync: you just set `\"exports\"` once using your custom condition, then tell your other tools (including TypeScript itself) to pay attention to that condition.\n\nAs usual the \"comments section\" for this post is on Twitter. Feel free to make comments or ask questions in the replies to this tweet:\n\n\u003cblockquote class=\"twitter-tweet\"\u003e\u003cp lang=\"en\" dir=\"ltr\"\u003enew blog post ð it breaks down a few approaches to configuring \u0026quot;live types\u0026quot; in TypeScript monorepos. you should never need to run `build` while developing!\u003cbr\u003e\u003cbr\u003e1. tsconfig paths\u003cbr\u003e2. custom export conditions\u003cbr\u003e3. publishConfig (*my recommended solution)\u003ca href=\"https://t.co/sf6F3CfcsQ\"\u003ehttps://t.co/sf6F3CfcsQ\u003c/a\u003e\u003c/p\u003e\u0026mdash; Colin McDonnell (@colinhacks) \u003ca href=\"https://twitter.com/colinhacks/status/1796595123595378822?ref_src=twsrc%5Etfw\"\u003eMay 31, 2024\u003c/a\u003e\u003c/blockquote\u003e \u003cscript async src=\"https://platform.twitter.com/widgets.js\" charset=\"utf-8\"\u003e\u003c/script\u003e\n\nHappy monorepo hacking!\n"},{"rssId":"/essays/email-regex-reasonable","relativeUrl":"/essays/reasonable-email-regex","url":"https://colinhacks.com/essays/reasonable-email-regex","title":"An email regex for reasonable people","subtitle":null,"published":true,"description":null,"canonicalUrl":"https://colinhacks.com/essays/reasonable-email-regex","publishedAt":1678256016682,"updatedAt":1742774026284,"tags":["TypeScript"],"author":"Colin McDonnell","authorHandle":"colinhacks","authorImage":"https://colinhacks.com/headshot_small.jpg","bannerImage":"https://colinhacks.com/envelopes.jpg","thumbImage":"https://colinhacks.com/envelopes_small.jpg","content":"\nI maintain Zod, which is a schema library. Zod lets people declare an email schema like this:\n\n```ts\nimport {z} from 'zod';\n\nz.string().email();\n```\n\nA lot of people think every _technically valid_ email address should pass validation by that schema. I [used to think that](https://github.com/colinhacks/zod/issues/639#issuecomment-917223045) too.\n\nOver the years I've merged several PRs to make the email regex more \"technically correct\". Most recently, I merged a PR that adds support for _IPv6 addresses_ as the domain part of an email. They look like this and I hate them:\n\n```\njonny@[ipv6:7e95:0559:10f2:21e9:9dab:7309:c116:ca3b]\n```\n\nTurns out that PR also broke plain old subdomains: `[email protected]`. That means Zod currently fails to parse my _mom's current email address_ but Jonny up there can parse his freakish IPv6 email.\n\nThis caused a spiritual crisis and made me re-evaluate my whole stance on what `z.string().email()` should do. Zod's users are mostly engineers who are building apps. When you're building an app, you want to make sure your users are providing normal-ass email addresses. So that's what `z.string().email()` is going do.\n\nSo I rewrote the regex from scratch to be simple and reasonable. Here's what I came up with:\n\n```ts\n/^(?!\\.)(?!.*\\.\\.)([a-z0-9_'+\\-\\.]*)[a-z0-9_+\\-]@([a-z0-9][a-z0-9\\-]*\\.)+[a-z]{2,}$/i;\n```\n\nLet's break that down in plain English:\n\n**The username** (AKA \"local part\")\n\n- can use letters, numbers, and these special characters: `_'+-.`\n- can't have two dots in a row\n- can't start or end with a dot\n\n**The domain**\n\n- must consist of at least 2 dot-separated segments consisting of letters, numbers, and hyphens\n- can't have two dots in a row\n- must end with a \"TLD\" that's 2+ letters\n\nIt is _not_ trying to be [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322) compliant. It's not going to check if the TLD is real. And it's not going to implement any of this craziness:\n\n- No \"printable\" characters: `wtf-!#$%\u0026'*/=?^_{|}[email protected]`\n- No quoted local parts: `\"zod is cool\"@mail.com`\n- No comments: `regexiscool(kinda)@mail.com`\n- No IPv4: `billie@[1.2.3.4]`\n- No IPv6: `jonny@[ipv6:7e95:0559:10f2:21e9:9dab:7309:c116:ca3b]`\n- No emoji: `ð@mail.com`\n\nBut for reasonable people, it'll do.\n"}],"tags":["Zod","TypeScript","Validation","AI","Agents","Monorepos","Next.js","GraphQL","tRPC","React","Firebase","Proposals","Open Source Sustainability","Jamstack","UI De
111sign","Open Source","Node.js"]},"__N_SSG":true},"page":"/essays/[essay]","query":{"essay":"introducing-zod-codecs"},"buildId":"build-TfctsWXpff2fKS","isFallback":false,"gsp":true,"scriptLoader":[]}</script>
111</body></html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.