The first minor version since the introduction of Zod 4 back in May. This version contains a number of features that barely missed the cut for the 4.0 release. With Zod 4 stable and widely adopted, there's more time to resume feature development.
Codecs
This is the flagship feature of this release. Codecs are a new API & schema type that encapsulates a bi-directional transformation. It's a huge missing piece in Zod that's finally filled, and it unlocks some totally new ways to use Zod.
const stringToDate = z.codec(
z.iso.datetime(), // input schema: ISO date string
z.date(), // output schema: Date object
{
decode: (isoString) => new Date(isoString),
encode: (date) => date.toISOString(),
}
);
New top-level functions are added for processing inputs in the forward direction ("decoding") and backward direction ("encoding").
z.decode(stringToDate, "2025-08-21T20:59:45.500Z")
// => Date
z.encode(stringToDate, new Date())
// => "2025-08-21T20:59:45.500Z"
For bundle size reasons, these are only available as top-level functions instead of methods.
To parse data in the "forward" direction (AKA decode), you can continue using the existing .parse
method/function. Alternatively you can use the new top-level z.decode
function:
stringToDate.parse("2024-01-15T10:30:00.000Z"); // method form
z.parse(stringToDate, "2024-01-15T10:30:00.000Z"); // functional form
// => Date
z.decode(stringToDate, "2024-01-15T10:30:00.000Z")
// => Date
While z.decode()
behaves identically to z.parse()
in the example above, its inputs are strongly typed . That is, it expects an input of type string
, whereas .parse()
accepts unknown
.
z.parse(stringToDate, Symbol('not-a-string')); // functional form
// => fails at runtime, but no TypeScript error
z.decode(stringToDate, Symbol("not-a-string"));
// ^ ❌ Argument of type 'symbol' is not assignable to parameter of type 'Date'. ts(2345)
This is a highly requested feature unto itself:
To parse data in the "backward direction" (AKA encode), use the new top-level z.encode()
function.
z.encode(stringToDate, new Date())
// => "2025-08-21T20:59:45.500Z"
You can use any Zod schema with z.encode()
. The vast majority of Zod schemas are non-transforming (the input and output types are identical) so z.decode()
and z.encode()
behave identically. Only certain schema types change their behavior:
- Codecs — runs from
B->A
and executes theencode
transform during encoding - Pipes — these execute
B->A
instead ofA->B
- Defaults and prefaults — Only applied in the forward direction
- Catch — Only applied in the forward direction
Note — To avoid increasing bundle size unnecessarily, there are no method equivalents of
z.decode()
andz.encode()
. These are top-level functions only.
The usual async and safe variants exist as well:
// decode functions
z.decode(stringToDate, "2024-01-15T10:30:00.000Z")
await z.decodeAsync(stringToDate, "2024-01-15T10:30:00.000Z")
z.safeDecode(stringToDate, "2024-01-15T10:30:00.000Z")
await z.safeDecodeAsync(stringToDate, "2024-01-15T10:30:00.000Z")
// encode functions
z.encode(stringToDate, new Date())
await z.encodeAsync(stringToDate, new Date())
z.safeEncode(stringToDate, new Date())
await z.safeEncodeAsync(stringToDate, new Date())
Below are some "worked examples" for some commonly-needed codecs. These examples are all tested internally for correctness. Just copy/paste them into your project as needed. There is a more comprehensive set available at zod.dev/codecs.
stringToBigInt
Converts bigint
into a serializable form.
const stringToBigInt = z.codec(z.string(), z.bigint(), {
decode: (str) => BigInt(str),
encode: (bigint) => bigint.toString(),
});
z.decode(stringToBigInt, "12345"); // => 12345n
z.encode(stringToBigInt, 12345n); // => "12345"
json
Parses/stringifies JSON data.
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 data, .pipe()
the result of this codec into another schema.
const Params = z.object({ name: z.string(), age: z.number() });
const JsonToParams = jsonCodec.pipe(Params);
z.decode(JsonToParams, '{"name":"Alice","age":30}'); // => { name: "Alice", age: 30 }
z.encode(JsonToParams, { name: "Bob", age: 25 }); // => '{"name":"Bob","age":25}'
Further reading
For more examples and a technical breakdown of how encoding works, read the full docs → That page contains implementations for several other commonly-needed codecs:
stringToNumber
stringToInt
stringToBigInt
numberToBigInt
isoDatetimeToDate
epochSecondsToDate
epochMillisToDate
jsonCodec
utf8ToBytes
bytesToUtf8
base64ToBytes
base64urlToBytes
hexToBytes
stringToURL
stringToHttpURL
uriComponent
stringToBoolean
.safeExtend()
The existing way to add additional fields to an object is to use .extend()
.
const A = z.object({ a: z.string() })
const B = A.extend({ b: z.string() })
Unfortunately this is a bit of a misnomer, as it allows you to overwrite existing fields. This means the result of .extend()
may not literally extend
the original type (in the TypeScript sense).
const A = z.object({ a: z.string() }) // { a: string }
const B = A.extend({ a: z.number() }) // { a: number }
To enforce true extends
logic, Zod 4.1 introduces a new .safeExtend()
method. This statically enforces that the newly added properties conform to the existing ones.
z.object({ a: z.string() }).safeExtend({ a: z.number().min(5) }); // ✅
z.object({ a: z.string() }).safeExtend({ a: z.any() }); // ✅
z.object({ a: z.string() }).safeExtend({ a: z.number() });
// ^ ❌ ZodNumber is not assignable
Importantly, this new API allows you to safely extend objects containing refinements.
const AB = z.object({ a: z.string(), b: z.string() }).refine(val => val.a === val.b);
const ABC = AB.safeExtend({ c: z.string() });
// ABC includes the refinements defined on AB
Previously (in Zod 4.x) any refinements attached to the base schema were dropped in the extended result. This was too unexpected. It now throws an error. (Zod 3 did not support extension of refined objects either.)
z.hash()
A new top-level string format for validating hashes produced using various common algorithms & encodings.
const md5Schema = z.hash("md5");
// => ZodCustomStringFormat<"md5_hex">
const sha256Base64 = z.hash("sha256", { enc: "base64" });
// => ZodCustomStringFormat<"sha256_base64">
The following hash algorithms and encodings are supported. Each cell provides information about the expected number of characters/padding.
Algorithm / Encoding | "hex"
| "base64"
| "base64url"
|
---|---|---|---|
"md5"
| 32 | 24 (22 + "==") | 22 |
"sha1"
| 40 | 28 (27 + "=") | 27 |
"sha256"
| 64 | 44 (43 + "=") | 43 |
"sha384"
| 96 | 64 (no padding) | 64 |
"sha512"
| 128 | 88 (86 + "==") | 86 |
z.hex()
To validate hexadecimal strings of any length.
const hexSchema = z.hex();
hexSchema.parse("123abc"); // ✅ "123abc"
hexSchema.parse("DEADBEEF"); // ✅ "DEADBEEF"
hexSchema.parse("xyz"); // ❌ ZodError