File: codecs.md | Updated: 11/16/2025
๐ Zod 4 is now stable! ย Read the announcement.
Copy markdown
โจ New โ Introduced in zod@4.1
All Zod schemas can process inputs in both the forward and backward direction:
Input to Output
.parse().decode()Output to Input
.encode()In most cases, this is a distinction without a difference. The input and output types are identical, so there's no difference between "forward" and "backward".
ZodZod Mini
const schema = z.string();
type Input = z.input<typeof schema>; // string
type Output = z.output<typeof schema>; // string
schema.parse("asdf"); // => "asdf"
schema.decode("asdf"); // => "asdf"
schema.encode("asdf"); // => "asdf"
However, some schema types cause the input and output types to diverge, notably z.codec(). Codecs are a special type of schema that defines a bi-directional transformation between two other schemas.
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
}
);
In these cases, z.decode() and z.encode() behave quite differently.
ZodZod Mini
stringToDate.decode("2024-01-15T10:30:00.000Z")
// => Date
stringToDate.encode(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.
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!
const payloadSchema = z.object({
startDate: stringToDate
});
payloadSchema.decode({
startDate: "2024-01-15T10:30:00.000Z"
}); // => { startDate: Date }
While .parse() and .decode() behave identically at runtime, they have different type signatures. The .parse() method accepts unknown as input, and returns a value that matches the schema's inferred output type. By contrast, the z.decode() and z.encode() functions have strongly-typed inputs.
stringToDate.parse(12345);
// no complaints from TypeScript (fails at runtime)
stringToDate.decode(12345);
// โ TypeScript error: Argument of type 'number' is not assignable to parameter of type 'string'.
stringToDate.encode(12345);
// โ TypeScript error: Argument of type 'number' is not assignable to parameter of type 'Date'.
Why the difference? Encoding and decoding imply transformation. In many cases, the inputs to these methods are already strongly typed in application code, so z.decode/z.encode accept strongly typed inputs to surface mistakes at compile time. Here's a diagram demonstrating the differences between the type signatures for parse(), decode(), and encode().


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().
stringToDate.decode("2024-01-15T10:30:00.000Z");
// => Date
stringToDate.decodeAsync("2024-01-15T10:30:00.000Z");
// => Promise<Date>
stringToDate.decodeSafe("2024-01-15T10:30:00.000Z");
// => { success: true, data: Date } | { success: false, error: ZodError }
stringToDate.decodeSafeAsync("2024-01-15T10:30:00.000Z");
// => Promise<{ success: true, data: Date } | { success: false, error: ZodError }>
There are some subtleties to how certain Zod schemas "reverse" their parse behavior.
This one is fairly self-explanatory. Codecs encapsulate a bi-directional transformation between two types. z.decode() triggers the decode transform to convert input into a parsed value, while z.encode() triggers the encode transform to serialize it back.
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
}
);
stringToDate.decode("2024-01-15T10:30:00.000Z");
// => Date
stringToDate.encode(new Date("2024-01-15"));
// => string
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.
All checks (.refine(), .min(), .max(), etc.) are still executed in both directions.
const schema = stringToDate.refine((date) => date.getFullYear() >= 2000, "Must be this millennium");
schema.encode(new Date("2000-01-01"));
// => Date
schema.encode(new Date("1999-01-01"));
// => โ ZodError: [\
// {\
// "code": "custom",\
// "path": [],\
// "message": "Must be this millennium"\
// }\
// ]
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();
schema.decode(" hello ");
// => "hello"
schema.encode(" hello ");
// => "hello"
Defaults and prefaults are only applied in the "forward" direction.
const stringWithDefault = z.string().default("hello");
stringWithDefault.decode(undefined);
// => "hello"
stringWithDefault.encode(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.
Similarly, .catch() is only applied in the "forward" direction.
const stringWithCatch = z.string().catch("hello");
stringWithCatch.decode(1234);
// => "hello"
stringWithCatch.encode(1234);
// => ZodError: Expected string, received number
Note โ Stringbool 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();
stringbool.decode("true"); // => true
stringbool.decode("false"); // => false
stringbool.encode(true); // => "true"
stringbool.encode(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"] });
stringbool.encode(true); // => "yes"
stringbool.encode(false); // => "no"
โ ๏ธ โ 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);
schema.encode(1234);
// โ Error: Encountered unidirectional transform during encode: ZodTransform
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.
stringToNumberConverts 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(),
});
stringToNumber.decode("42.5"); // => 42.5
stringToNumber.encode(42.5); // => "42.5"
stringToIntConverts 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(),
});
stringToInt.decode("42"); // => 42
stringToInt.encode(42); // => "42"
stringToBigIntConverts string representations to JavaScript bigint type.
const stringToBigInt = z.codec(z.string(), z.bigint(), {
decode: (str) => BigInt(str),
encode: (bigint) => bigint.toString(),
});
stringToBigInt.decode("12345"); // => 12345n
stringToBigInt.encode(12345n); // => "12345"
numberToBigIntConverts JavaScript number to bigint type.
const numberToBigInt = z.codec(z.int(), z.bigint(), {
decode: (num) => BigInt(num),
encode: (bigint) => Number(bigint),
});
numberToBigInt.decode(42); // => 42n
numberToBigInt.encode(42n); // => 42
isoDatetimeToDateConverts 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(),
});
isoDatetimeToDate.decode("2024-01-15T10:30:00.000Z"); // => Date object
isoDatetimeToDate.encode(new Date("2024-01-15")); // => "2024-01-15T00:00:00.000Z"
epochSecondsToDateConverts 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),
});
epochSecondsToDate.decode(1705314600); // => Date object
epochSecondsToDate.encode(new Date()); // => Unix timestamp in seconds
epochMillisToDateConverts 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(),
});
epochMillisToDate.decode(1705314600000); // => Date object
epochMillisToDate.encode(new Date()); // => Unix timestamp in milliseconds
json(schema)Parses JSON strings into structured data and serializes back to JSON. This generic function accepts an output schema to validate the parsed JSON data.
const jsonCodec = <T extends z.core.$ZodType>(schema: T) =>
z.codec(z.string(), schema, {
decode: (jsonString, ctx) => {
try {
return JSON.parse(jsonString);
} catch (err: any) {
ctx.issues.push({
code: "invalid_format",
format: "json",
input: jsonString,
message: err.message,
});
return z.NEVER;
}
},
encode: (value) => JSON.stringify(value),
});
Usage example with a specific schema:
const jsonToObject = jsonCodec(z.object({ name: z.string(), age: z.number() }));
jsonToObject.decode('{"name":"Alice","age":30}');
// => { name: "Alice", age: 30 }
jsonToObject.encode({ name: "Bob", age: 25 });
// => '{"name":"Bob","age":25}'
jsonToObject.decode('~~invalid~~');
// ZodError: [\
// {\
// "code": "invalid_format",\
// "format": "json",\
// "path": [],\
// "message": "Unexpected token '~', \"~~invalid~~\" is not valid JSON"\
// }\
// ]
utf8ToBytesConverts 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),
});
utf8ToBytes.decode("Hello, ไธ็!"); // => Uint8Array
utf8ToBytes.encode(bytes); // => "Hello, ไธ็!"
bytesToUtf8Converts 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),
});
bytesToUtf8.decode(bytes); // => "Hello, ไธ็!"
bytesToUtf8.encode("Hello, ไธ็!"); // => Uint8Array
base64ToBytesConverts base64 strings to Uint8Array byte arrays and vice versa.
const base64ToBytes = z.codec(z.base64(), z.instanceof(Uint8Array), {
decode: (base64String) => z.util.base64ToUint8Array(base64String),
encode: (bytes) => z.util.uint8ArrayToBase64(bytes),
});
base64ToBytes.decode("SGVsbG8="); // => Uint8Array([72, 101, 108, 108, 111])
base64ToBytes.encode(bytes); // => "SGVsbG8="
base64urlToBytesConverts base64url strings (URL-safe base64) to Uint8Array byte arrays.
const base64urlToBytes = z.codec(z.base64url(), z.instanceof(Uint8Array), {
decode: (base64urlString) => z.util.base64urlToUint8Array(base64urlString),
encode: (bytes) => z.util.uint8ArrayToBase64url(bytes),
});
base64urlToBytes.decode("SGVsbG8"); // => Uint8Array([72, 101, 108, 108, 111])
base64urlToBytes.encode(bytes); // => "SGVsbG8"
hexToBytesConverts hexadecimal strings to Uint8Array byte arrays and vice versa.
const hexToBytes = z.codec(z.hex(), z.instanceof(Uint8Array), {
decode: (hexString) => z.util.hexToUint8Array(hexString),
encode: (bytes) => z.util.uint8ArrayToHex(bytes),
});
hexToBytes.decode("48656c6c6f"); // => Uint8Array([72, 101, 108, 108, 111])
hexToBytes.encode(bytes); // => "48656c6c6f"
stringToURLConverts URL strings to JavaScript URL objects.
const stringToURL = z.codec(z.url(), z.instanceof(URL), {
decode: (urlString) => new URL(urlString),
encode: (url) => url.href,
});
stringToURL.decode("https://example.com/path"); // => URL object
stringToURL.encode(new URL("https://example.com")); // => "https://example.com/"
stringToHttpURLConverts 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,
});
stringToHttpURL.decode("https://api.example.com/v1"); // => URL object
stringToHttpURL.encode(url); // => "https://api.example.com/v1"
uriComponentEncodes and decodes URI components using encodeURIComponent() and decodeURIComponent().
const uriComponent = z.codec(z.string(), z.string(), {
decode: (encodedString) => decodeURIComponent(encodedString),
encode: (decodedString) => encodeURIComponent(decodedString),
});
uriComponent.decode("Hello%20World%21"); // => "Hello World!"
uriComponent.encode("Hello World!"); // => "Hello%20World!"
JSON Schema
How to convert Zod schemas to JSON Schema
Ecosystem
Overview of the Zod ecosystem including integrations, tools, and community resources
Composability
Type-safe inputs
Async and safe variants
How encoding works
Codecs
Pipes
Refinements
Defaults and prefaults
Catch
Stringbool
Transforms
Useful codecs
stringToNumber
stringToInt
stringToBigInt
numberToBigInt
isoDatetimeToDate
epochSecondsToDate
epochMillisToDate
json(schema)
utf8ToBytes
bytesToUtf8
base64ToBytes
base64urlToBytes
hexToBytes
stringToURL
stringToHttpURL
uriComponent