How to Use Zod Encode Decode Methods for Bidirectional Data Transformation

Zod's encode and decode methods let you define bidirectional transformations between input and output schemas, enabling safe serialization and deserialization with full type safety.

The colinhacks/zod library provides a powerful codec API that pairs input and output schemas with reversible transformation logic. Unlike unidirectional transforms, Zod encode decode methods maintain type safety in both directions—parsing raw input into refined types and serializing them back to their original format.

Understanding Zod's Codec Architecture

Zod implements bidirectional parsing through a direction-aware engine in its core parsing module. When you invoke encode or decode, the library creates a parse context that determines which transformation direction to apply.

Core Components in core/parse.ts

The low-level implementation resides in packages/zod/src/v4/core/parse.ts. This file exports the fundamental encode and decode functions that power the public API:

  • decode – Forwards to the regular parser with the default forward direction (packages/zod/src/v4/core/parse.ts#L110-L118)
  • encode – Forwards to the parser with a direction: "backward" flag (packages/zod/src/v4/core/parse.ts#L96-L108)

The public façade in packages/zod/src/v4/classic/parse.ts re-exports these as top-level z.encode and z.decode utilities (packages/zod/src/v4/classic/parse.ts#L35-L41).

The Direction Flag

When encode creates a parse context with { direction: "backward" }, all transforms check ctx.direction to determine which half of a bidirectional transformation to execute. This prevents the "unidirectional transform" errors that occur when trying to serialize data that only has a one-way conversion defined.

Creating Your First Codec with Zod Encode Decode Methods

A codec combines an input schema, output schema, and transformation callbacks. The builder in packages/zod/src/v4/mini/codec.ts wires these components together.

import * as z from "zod/mini";
import { en } from "zod/locales";

z.config(en());

// Define bidirectional transformation: ISO string ↔ Date
const isoDateCodec = z.codec(
  z.iso.datetime(), // input schema: validated ISO string
  z.date(),         // output schema: Date instance
  {
    decode: (iso) => new Date(iso),        // forward: string → Date
    encode: (date) => date.toISOString(), // backward: Date → string
  }
);

Once defined, you can transform data in both directions:

// Decode: raw string → Date
const dateObj = z.decode(isoDateCodec, "2024-01-15T10:30:00.000Z");
// Result: Date object

// Encode: Date → string
const isoString = z.encode(isoDateCodec, dateObj);
// Result: "2024-01-15T10:30:00.000Z"

Practical Usage Patterns

Basic String to Date Transformation

The ISO datetime codec represents the most common use case—converting between wire formats (strings) and runtime types (Date objects). The input schema validates the string format while the output schema ensures the result is a proper Date instance.

Async Encode and Decode Operations

When transformations involve asynchronous operations—such as database lookups or external API calls—use the async variants:

// Async decode (returns a Promise)
const asyncDecoded = await z.decodeAsync(
  isoDateCodec, 
  "2024-01-15T10:30:00.000Z"
);

// Async encode
const asyncEncoded = await z.encodeAsync(
  isoDateCodec, 
  new Date()
);

These functions forward to _parseAsync in the core engine, allowing transforms to return Promises that resolve before validation continues.

Safe Error Handling with safeEncode and safeDecode

For error-handling without try-catch blocks, use the safe variants that return result objects:

// Safe decode returns { success, data?, error? }
const safe = z.safeDecode(isoDateCodec, "invalid-date");
if (!safe.success) {
  console.error(safe.error.issues);
  // Handle validation errors
}

// Safe encode
const safeEnc = z.safeEncode(isoDateCodec, new Date("2024-01-01"));
if (safeEnc.success) {
  console.log(safeEnc.data); // ISO string
}

These methods wrap the underlying parse calls in error boundaries, returning a discriminated union that TypeScript can narrow.

Nested Codecs in Object Schemas

Codecs compose within larger schemas, allowing complex transformations at specific fields:

const waypointSchema = z.object({
  name: z.string().check(z.minLength(1, "Waypoint name required")),
  difficulty: z.enum(["easy", "medium", "hard"]),
  coordinate: z.codec(
    z.string().check(z.regex(/^-?\d+,-?\d+$/, "Must be 'x,y' format")),
    z.object({ x: z.number(), y: z.number() }),
    {
      decode: (s) => {
        const [x, y] = s.split(",").map(Number);
        return { x, y };
      },
      encode: (obj) => `${obj.x},${obj.y}`,
    }
  ),
});

const raw = {
  name: "Peak",
  difficulty: "hard",
  coordinate: "12,34",
};

// Decode entire object including nested codec
const decoded = z.decode(waypointSchema, raw);
// Encode back to wire format
const encoded = z.encode(waypointSchema, decoded);

Advanced Configuration and Edge Cases

Handling Unidirectional Transforms

Standard z.transform creates unidirectional conversions that only work with decode. If you attempt to encode through a schema containing a unidirectional transform, Zod throws a ZodEncodeError according to the test suite in packages/zod/src/v4/classic/tests/transform.test.ts (line 356).

Always use z.codec with explicit encode and decode callbacks when you need bidirectional transformation.

Performance Considerations

The synchronous encode and decode methods offer better performance than their async counterparts when transformations are pure computations. The core engine in packages/zod/src/v4/core/parse.ts optimizes the synchronous path by avoiding Promise overhead.

For high-throughput applications, prefer synchronous codecs and reserve decodeAsync/encodeAsync for operations that genuinely require I/O.

Summary

  • Zod encode decode methods provide bidirectional data transformation through the codec API, coupling input and output schemas with reversible conversion logic.
  • The architecture uses a direction flag in packages/zod/src/v4/core/parse.ts to distinguish between forward (decode) and backward (encode) transformations.
  • Create codecs using z.codec(inputSchema, outputSchema, { decode, encode }) from the mini API or access top-level utilities via z.encode and z.decode in the classic API.
  • Handle asynchronous transformations with decodeAsync and encodeAsync, and use safeDecode and safeEncode for error handling without exceptions.
  • Avoid unidirectional z.transform when encoding is required, as it triggers a ZodEncodeError during backward parsing.

Frequently Asked Questions

What is the difference between encode and decode in Zod?

Decode transforms raw input data into a refined output type (forward direction), such as parsing an ISO string into a Date object. Encode reverses this process, converting the refined output back into the raw input format (backward direction), such as serializing a Date back to an ISO string. The core parser in packages/zod/src/v4/core/parse.ts distinguishes these operations using a direction flag.

How do I handle errors when using Zod encode decode methods?

Use the safeDecode and safeEncode utilities to avoid throwing exceptions. These functions return a discriminated union with success, data, and error properties. If success is false, the error property contains a ZodError with detailed issue information. This pattern is particularly useful in API handlers where you need to return structured error responses.

Can I use async transformations with Zod codecs?

Yes, Zod provides decodeAsync and encodeAsync for transformations that return Promises. These functions are available in both the core and classic APIs. They forward to the internal _parseAsync engine, allowing you to perform database lookups, API calls, or other asynchronous operations during encoding or decoding. Use these when your transformation logic requires I/O.

What happens if I try to encode a unidirectional transform?

Attempting to encode through a schema that uses z.transform without a reverse callback throws a ZodEncodeError. According to the test suite in packages/zod/src/v4/classic/tests/transform.test.ts, unidirectional transforms only support the forward (decode) direction. To enable bidirectional transformation, use z.codec with explicit encode and decode callbacks instead of z.transform.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →