💎 Zod 4 is now stable!  Read the announcement.
Zod logo

Codecs

New — Introduced in [email protected]

TLDR — You can use z.encode(<schema>, <input>) to perform an encode operation ("reverse parsing"). This will work with any schema but is primarily intended for use in conjunction with z.codec().

This most important exception is the .transform() API, which performs a unidirectional transformation. This isn't compatible with z.encode() as Zod cannot reverse it in a sound way. If you use .transform() anywhere in your schema, attempting a z.encode() operation will throw a runtime error (not a ZodError). To fix this, you'll need to refactor all usage of .transform() to use z.codec().

All Zod schemas (with one major exception described below) are "codecs". That is, they can process inputs in both the forward and backward direction:

  • Decoding: the "forward" direction: Input -> Output. This is what regular .parse() does.
  • Encoding: the "backward" direction: Output -> Input.

You probably already know how to parse data with Zod:

import * as z from "zod";
 
const mySchema = z.string();
 
// method form
mySchema.parse("hello"); // => "hello"
 
// functional form
z.parse(mySchema, "hello"); // => "hello"

Zod also provides dedicated functions for performing "decode" and "encode" operations.

z.decode(mySchema, "hello"); // => "hello"
z.encode(mySchema, "hello"); // => "hello"

In many cases (such as the string schema above), the input and output types of a Zod schema are identical, so z.decode() and z.encode() are functionally equivalent. But some schema types cause the input and output types to diverge:

  • z.default() (input is optional, output is not)
  • z.transform() (a unidirectional transformation)
  • z.codec() (bidirectional transformation)

Most important of these is z.codec(), which is Zod's primary mechanism for defining bidirectional transformations.

const stringToDate = z.codec(
  z.iso.datetime(),  // input schema: ISO date string
  z.date(),          // output schema: Date object
  {
    decode: (isoString) => new Date(isoString), // ISO string → Date
    encode: (date) => date.toISOString(),       // Date → ISO string
  }
);
 
type Input = z.input<typeof stringToDate>;   // => string
type Output = z.output<typeof stringToDate>; // => Date

In these cases, z.decode() and z.encode() behave quite differently.

const payloadSchema = z.object({ startDate: stringToDate });
 
z.decode(stringToDate, "2024-01-15T10:30:00.000Z")
// => Date
 
z.encode(stringToDate, new Date("2024-01-15T10:30:00.000Z"))
// => string

Note —There's nothing special about the directions or terminology here. Instead of encoding with an A -> B codec, you could instead decode with a B -> A codec. The use of the terms "decode" and "encode" is just a convention.

This is particularly useful when parsing data at a network boundary. You can share a single Zod schema between your client and server, then use this single schema to convert between a network-friendly format (say, JSON) and a richer JavaScript representation.

Codecs encoding and decoding data across a network boundary

Composability

Note — You can use z.encode() and z.decode() with any schema. It doesn't have to be a ZodCodec.

Codecs are a schema like any other. You can nest them inside objects, arrays, pipes, etc. There are no rules on where you can use them!

Type-safe inputs

The usual .parse() method accepts unknown as input, and returns a value that matches the schema's inferred output type.

By constrast, the z.decode() and z.encode() functions have strongly-typed inputs.

stringToDate.parse(12345); 
// no complaints from TypeScript (but it will fail at runtime)
 
z.decode(stringToDate, 12345); 
// ❌ TypeScript error: Argument of type 'number' is not assignable to parameter of type 'string'.
 
z.encode(stringToDate, 12345); 
// ❌ TypeScript error: Argument of type 'number' is not assignable to parameter of type 'Date'.

Here's a diagram demonstrating the differences between the type signatures for parse(), decode(), and encode().

Codec directionality diagram showing bidirectional transformation between input and output schemas

Async and safe variants

As with .transform() and .refine(), codecs support async transforms.

const asyncCodec = z.codec(z.string(), z.number(), {
  decode: async (str) => Number(str),
  encode: async (num) => num.toString(),
});

As with regular parse(), there are "safe" and "async" variants of decode() and encode().

z.decode(stringToDate, "2024-01-15T10:30:00.000Z"); 
// => Date
 
z.decodeAsync(stringToDate, "2024-01-15T10:30:00.000Z"); 
// => Promise<Date>
 
z.decodeSafe(stringToDate, "2024-01-15T10:30:00.000Z"); 
// => { success: true, data: Date } | { success: false, error: ZodError }
 
z.decodeSafeAsync(stringToDate, "2024-01-15T10:30:00.000Z"); 
// => Promise<{ success: true, data: Date } | { success: false, error: ZodError }>

How encoding works

There are some subtleties to how certain Zod schemas "reverse" their parse behavior.

Codecs

This one is fairly self-explanatory. Codecs encapsulate a bi-directional transformation between two types. During z.decode(), the decode transform is executed. During z.encode(), the encode transform is executed.

const stringToDate = z.codec(
  z.iso.datetime(),  // input schema: ISO date string
  z.date(),          // output schema: Date object
  {
    decode: (isoString) => new Date(isoString), // ISO string → Date
    encode: (date) => date.toISOString(),       // Date → ISO string
  }
);
 
z.decode(stringToDate, "2024-01-15T10:30:00.000Z"); 
// => Date
 
z.encode(stringToDate, new Date("2024-01-15")); 
// => string

Pipes

Fun fact — Codecs are actually implemented internally as subclass of pipes that have been augmented with "interstitial" transform logic.

During regular decoding, a ZodPipe<A, B> schema will first parse the data with A, then pass it into B. As you might expect, during encoding, the data is first encoded with B, then passed into A.

Refinements

All checks (.refine(), .min(), .max(), etc.) are still executed in both directions.

const schema = stringToDate.refine((date) => date.getFullYear() > 2000, "Must be this millenium");
 
z.encode(schema, new Date("2000-01-01"));
// => Date
 
z.encode(schema, new Date("1999-01-01"));
// => ❌ ZodError: [
//   {
//     "code": "custom",
//     "path": [],
//     "message": "Must be this millenium"
//   }
// ]

To avoid unexpected errors in your custom .refine() logic, Zod performs two "passes" during z.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.

This approach also supports "mutating transforms" like z.string().trim() or z.string().toLowerCase():

const schema = z.string().trim();
 
z.decode(schema, "  hello  ");
// => "hello"
 
z.encode(schema, "  hello  ");
// => "hello"

Defaults and prefaults

Defaults and prefaults are only applied in the "forward" direction.

const stringWithDefault = z.string().default("hello");
 
z.decode(stringWithDefault, undefined); 
// => "hello"
 
z.encode(stringWithDefault, undefined); 
// => ZodError: Expected string, received undefined

When you attach a default value to a schema, the input becomes optional (| undefined) but the output does not. As such, undefined is not a valid input to z.encode() and defaults/prefaults will not be applied.

Catch

Similarly, .catch() is only applied in the "forward" direction.

const stringWithCatch = z.string().catch("hello");
 
z.decode(stringWithCatch, 1234); 
// => "hello"
 
z.encode(stringWithCatch, 1234); 
// => ZodError: Expected string, received number

Stringbool

NoteStringbool pre-dates the introduction of codecs in Zod. It has since been internally re-implemented as a codec.

The 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 z.encode()..

const stringbool = z.stringbool();
 
z.decode(stringbool, "true");  // => true
z.decode(stringbool, "false"); // => false
 
z.encode(stringbool, true);    // => "true"
z.encode(stringbool, false);   // => "false"

If you specify a custom set of truthy and falsy values, the first element in the array will be used instead.

const stringbool = z.stringbool({ truthy: ["yes", "y"], falsy: ["no", "n"] });
 
z.encode(stringbool, true);    // => "yes"
z.encode(stringbool, false);   // => "no"

Transforms

⚠️ — The .transform() API implements a unidirectional transformation. If any .transform() exists anywhere in your schema, attempting a z.encode() operation will throw a runtime error (not a ZodError).

const schema = z.string().transform(val => val.length);
 
z.encode(schema, 1234); 
// ❌ Error: Encountered unidirectional transform during encode: ZodTransform

Useful codecs

Below are implementations for a bunch of commonly-needed codecs. For the sake of customizability, these are not included as first-class APIs in Zod itself. Instead, you should copy/paste them into your project and modify them as needed.

Note — All of these codec implementations have been tested for correctness.

stringToNumber

Converts string representations of numbers to JavaScript number type using parseFloat().

const stringToNumber = z.codec(z.string().regex(z.regexes.number), z.number(), {
  decode: (str) => Number.parseFloat(str),
  encode: (num) => num.toString(),
});
 
z.decode(stringToNumber, "42.5");  // => 42.5
z.encode(stringToNumber, 42.5);    // => "42.5"

stringToInt

Converts string representations of integers to JavaScript number type using parseInt().

const stringToInt = z.codec(z.string().regex(z.regexes.integer), z.int(), {
  decode: (str) => Number.parseInt(str, 10),
  encode: (num) => num.toString(),
});
 
z.decode(stringToInt, "42");  // => 42
z.encode(stringToInt, 42);    // => "42"

stringToBigInt

Converts string representations to JavaScript bigint type.

const stringToBigInt = z.codec(z.string(), z.bigint(), {
  decode: (str) => BigInt(str),
  encode: (bigint) => bigint.toString(),
});
 
z.decode(stringToBigInt, "123456789012345678901234567890");  // => 123456789012345678901234567890n
z.encode(stringToBigInt, 123456789012345678901234567890n);   // => "123456789012345678901234567890"

numberToBigInt

Converts JavaScript number to bigint type.

const numberToBigInt = z.codec(z.int(), z.bigint(), {
  decode: (num) => BigInt(num),
  encode: (bigint) => Number(bigint),
});
 
z.decode(numberToBigInt, 42);   // => 42n
z.encode(numberToBigInt, 42n);  // => 42

isoDatetimeToDate

Converts ISO datetime strings to JavaScript Date objects.

const isoDatetimeToDate = z.codec(z.iso.datetime(), z.date(), {
  decode: (isoString) => new Date(isoString),
  encode: (date) => date.toISOString(),
});
 
z.decode(isoDatetimeToDate, "2024-01-15T10:30:00.000Z");  // => Date object
z.encode(isoDatetimeToDate, new Date("2024-01-15"));       // => "2024-01-15T00:00:00.000Z"

epochSecondsToDate

Converts Unix timestamps (seconds since epoch) to JavaScript Date objects.

const epochSecondsToDate = z.codec(z.int().min(0), z.date(), {
  decode: (seconds) => new Date(seconds * 1000),
  encode: (date) => Math.floor(date.getTime() / 1000),
});
 
z.decode(epochSecondsToDate, 1705314600);  // => Date object
z.encode(epochSecondsToDate, new Date());  // => Unix timestamp in seconds

epochMillisToDate

Converts Unix timestamps (milliseconds since epoch) to JavaScript Date objects.

const epochMillisToDate = z.codec(z.int().min(0), z.date(), {
  decode: (millis) => new Date(millis),
  encode: (date) => date.getTime(),
});
 
z.decode(epochMillisToDate, 1705314600000);  // => Date object
z.encode(epochMillisToDate, new Date());     // => Unix timestamp in milliseconds

jsonCodec

Parses JSON strings into structured data and serializes back to JSON.

// uses JSON.parse()/JSON.stringify() to transform between string and JSON
const jsonCodec = z.codec(z.string(), z.json(), {
  decode: (jsonString, ctx) => {
    try {
      return JSON.parse(jsonString);
    } catch (err: any) {
      ctx.issues.push({
        code: "invalid_format",
        format: "json_string",
        input: jsonString,
        message: err.message,
      });
      return z.NEVER;
    }
  },
  encode: (value) => JSON.stringify(value),
});

To further validate the resulting JSON data, pipe the result into another schema.

const jsonStringToData = jsonCodec.pipe(z.object({ name: z.string(), age: z.number() }));
 
z.decode(jsonStringToData, '~~invalid~~'); 
// ZodError: [
//   {
//     "code": "invalid_format",
//     "format": "json",
//     "path": [],
//     "message": "Invalid json"
//   }
// ]
 
z.decode(myCodec, '{"name":"Alice","age":30}');  
// => { name: "Alice", age: 30 }
 
z.encode(myCodec, { name: "Bob", age: 25 });     
// => '{"name":"Bob","age":25}'

utf8ToBytes

Converts UTF-8 strings to Uint8Array byte arrays.

const utf8ToBytes = z.codec(z.string(), z.instanceof(Uint8Array), {
  decode: (str) => new TextEncoder().encode(str),
  encode: (bytes) => new TextDecoder().decode(bytes),
});
 
z.decode(utf8ToBytes, "Hello, 世界!");  // => Uint8Array
z.encode(utf8ToBytes, bytes);          // => "Hello, 世界!"

bytesToUtf8

Converts Uint8Array byte arrays to UTF-8 strings.

const bytesToUtf8 = z.codec(z.instanceof(Uint8Array), z.string(), {
  decode: (bytes) => new TextDecoder().decode(bytes),
  encode: (str) => new TextEncoder().encode(str),
});
 
z.decode(bytesToUtf8, bytes);          // => "Hello, 世界!"
z.encode(bytesToUtf8, "Hello, 世界!");  // => Uint8Array

base64ToBytes

Converts base64 strings to Uint8Array byte arrays and vice versa.

const base64ToBytes = z.codec(z.base64(), z.instanceof(Uint8Array), {
  decode: (base64String) => z.core.util.base64ToUint8Array(base64String),
  encode: (bytes) => z.core.util.uint8ArrayToBase64(bytes),
});
 
z.decode(base64ToBytes, "SGVsbG8=");  // => Uint8Array([72, 101, 108, 108, 111])
z.encode(base64ToBytes, bytes);       // => "SGVsbG8="

base64urlToBytes

Converts base64url strings (URL-safe base64) to Uint8Array byte arrays.

const base64urlToBytes = z.codec(z.base64url(), z.instanceof(Uint8Array), {
  decode: (base64urlString) => z.core.util.base64urlToUint8Array(base64urlString),
  encode: (bytes) => z.core.util.uint8ArrayToBase64url(bytes),
});
 
z.decode(base64urlToBytes, "SGVsbG8");  // => Uint8Array([72, 101, 108, 108, 111])
z.encode(base64urlToBytes, bytes);      // => "SGVsbG8"

hexToBytes

Converts hexadecimal strings to Uint8Array byte arrays and vice versa.

const hexToBytes = z.codec(z.hex(), z.instanceof(Uint8Array), {
  decode: (hexString) => z.core.util.hexToUint8Array(hexString),
  encode: (bytes) => z.core.util.uint8ArrayToHex(bytes),
});
 
z.decode(hexToBytes, "48656c6c6f");     // => Uint8Array([72, 101, 108, 108, 111])
z.encode(hexToBytes, bytes);            // => "48656c6c6f"

stringToURL

Converts URL strings to JavaScript URL objects.

const stringToURL = z.codec(z.url(), z.instanceof(URL), {
  decode: (urlString) => new URL(urlString),
  encode: (url) => url.href,
});
 
z.decode(stringToURL, "https://example.com/path");  // => URL object
z.encode(stringToURL, new URL("https://example.com"));  // => "https://example.com/"

stringToHttpURL

Converts HTTP/HTTPS URL strings to JavaScript URL objects.

const stringToHttpURL = z.codec(z.httpUrl(), z.instanceof(URL), {
  decode: (urlString) => new URL(urlString),
  encode: (url) => url.href,
});
 
z.decode(stringToHttpURL, "https://api.example.com/v1");  // => URL object
z.encode(stringToHttpURL, url);                           // => "https://api.example.com/v1"

uriComponent

Encodes and decodes URI components using encodeURIComponent() and decodeURIComponent().

const uriComponent = z.codec(z.string(), z.string(), {
  decode: (encodedString) => decodeURIComponent(encodedString),
  encode: (decodedString) => encodeURIComponent(decodedString),
});
 
z.decode(uriComponent, "Hello%20World%21");  // => "Hello World!"
z.encode(uriComponent, "Hello World!");      // => "Hello%20World!"

stringToBoolean()

Converts string representations to boolean values. This is an alias for z.stringbool().

const stringToBoolean = (options?: { truthy?: string[]; falsy?: string[] }) =>
  z.stringbool(options);
 
const codec = stringToBoolean();
z.decode(codec, "true");   // => true
z.decode(codec, "false");  // => false
z.encode(codec, true);     // => "true"
z.encode(codec, false);    // => "false"
 
// With custom options
const customCodec = stringToBoolean({ truthy: ["yes", "y"], falsy: ["no", "n"] });
z.decode(customCodec, "yes");  // => true
z.encode(customCodec, true);   // => "yes"