# How to Use Zod Pipeline Transform for Chained Schema Validation

> Discover how to use Zod pipeline transform for chained schema validation. Compose schemas for type-safe transformations with automatic error short-circuiting. Learn more now.

- Repository: [Colin McDonnell/zod](https://github.com/colinhacks/zod)
- Tags: tutorial
- Published: 2026-02-23

---

**Zod's pipeline composes two schemas so that the output of the first becomes the input of the second, enabling type-safe chained transformations with automatic error short-circuiting.**

The **Zod pipeline transform** feature allows you to chain validation and transformation logic declaratively. According to the Zod v3 source code, this is implemented through the `ZodPipeline` class in [`packages/zod/src/v3/types.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v3/types.ts), which orchestrates the flow between an input schema and an output schema while handling both synchronous and asynchronous execution paths.

## What Is the Zod Pipeline Transform?

A **pipeline** in Zod is a schema type that links two distinct schemas: an `in` schema that receives the raw input, and an `out` schema that receives the transformed result. This architecture powers the `.pipe()` method you use when chaining transformations.

The pipeline is created via the static factory `ZodPipeline.create(a, b)` (lines 4777-4837 in [`packages/zod/src/v3/types.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v3/types.ts)). The class stores the two schemas as properties and implements the `_parse` method to handle the execution flow. Additionally, Zod re-exports a helper function `pipeline` from the same file (lines 5069-5079) that provides a functional API equivalent to the method chaining approach.

## How Zod Pipeline Works Under the Hood

### The ZodPipeline Class Structure

The `ZodPipeline` class extends `ZodType` and maintains two generic type parameters representing the input and output types. Internally, it holds:

- **`in`**: The input schema (the one you called `.transform()` on)
- **`out`**: The output schema passed to `.pipe()`

This structure ensures type safety across the transformation boundary, as the output type of the first schema must be compatible with the input type of the second.

### Parsing Flow and Short-Circuit Behavior

The `_parse` method implements a strict short-circuiting mechanism to prevent invalid data from reaching subsequent transformations. According to the source code analysis of lines 4835-4845 in [`types.ts`](https://github.com/colinhacks/zod/blob/main/types.ts):

1. **Sync Path**: The method first parses the `in` schema using `_parseSync`. If this returns an **aborted** status (indicating a fatal validation error), the entire pipeline aborts immediately. If the result is **dirty** (meaning a transformation succeeded but flagged the value as modified), the pipeline marks itself dirty and returns the value without passing it to the output schema. Otherwise, the clean value proceeds to `out._parseSync`.

2. **Async Path**: The logic mirrors the synchronous path but uses `_parseAsync` to handle promises. This allows pipelines to work with asynchronous transformations such as database lookups or API calls.

This design guarantees that **failed refinements**, **exceptions in transforms**, or **dirty flags** halt further processing, preventing type errors downstream.

## Practical Examples of Zod Pipeline Transform

### Synchronous String-to-Number Conversion

The most common use case involves parsing a string and transforming it into a validated number:

```typescript
import * as z from "zod/v3";

const schema = z.string()
  .transform(Number)          // "123" → 123 (returns ZodNumber internally)
  .pipe(z.number().int());  // enforce integer constraint on the output

schema.parse("42"); // → 42
// schema.parse("42.5") throws because .int() rejects non-integers

```

As implemented in `ZodPipeline` (lines 4777-4830 in [`packages/zod/src/v3/types.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v3/types.ts)), the `.pipe()` method creates a new pipeline instance linking the transformed string schema to the integer validation schema.

### Asynchronous Data Fetching

Pipelines support async transformations, enabling you to validate IDs and fetch corresponding records:

```typescript
import * as z from "zod/v3";

const asyncSchema = z.string()
  .transform(async (val) => {
    // Simulate database lookup
    const record = await fetchUserById(val);
    return record.age;
  })
  .pipe(z.number().positive());

const result = await asyncSchema.parseAsync("user-123"); // → 25

```

The async branch in `_parse` (lines 4835-4845) handles the promise resolution before passing the value to the output schema's validation logic.

### Error Short-Circuiting with Refinements

The pipeline aborts early when refinements fail, preventing unnecessary computation:

```typescript
import * as z from "zod/v3";

const strictSchema = z.string()
  .refine(v => v === "1234", { message: "must be 1234" })
  .transform(async v => Number(v))
  .pipe(z.number().refine(v => v < 100, { message: "too large" }));

// Fails at first refinement – transform never executes
strictSchema.safeParse("9999").error?.issues[0].message; // "must be 1234"

// Passes first refinement but fails second
strictSchema.safeParse("1234").error?.issues[0].message; // "too large"

```

This behavior is verified in the test suite at [`packages/zod/src/v3/tests/pipeline.test.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v3/tests/pipeline.test.ts), which demonstrates that refinement errors before the pipe stop further processing while allowing subsequent refinements to catch transformed values.

### Low-Level Pipeline API

For functional programming patterns, use the exported `pipeline` helper directly:

```typescript
import { pipeline, string, number } from "zod/v3";

const myPipe = pipeline(string(), number());
// Equivalent to: string().pipe(number())
myPipe.parse("7"); // → 7

```

This helper is re-exported from [`packages/zod/src/v3/types.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v3/types.ts) (lines 5069-5079) and provides the same functionality as the method chaining approach.

## Key Implementation Files

Understanding the pipeline architecture requires examining these specific locations in the Zod v3 codebase:

| File | Purpose | Location |
|------|---------|----------|
| [`packages/zod/src/v3/types.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v3/types.ts) | Defines `ZodPipeline` class, `_parse` logic, and `pipeline` export | Lines 4777-4837 (class), 5069-5079 (export) |
| [`packages/zod/src/v3/tests/pipeline.test.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v3/tests/pipeline.test.ts) | Test suite demonstrating sync, async, and error short-circuit behavior | Full file |

These files demonstrate that the pipeline feature is implemented as a first-class schema type with full support for Zod's parsing lifecycle, including dirty checking and abort semantics.

## Summary

- **Zod pipeline transform** composes two schemas where the output of the first becomes the input of the second, enabling chained validation and transformation logic.
- The implementation resides in `ZodPipeline` class within [`packages/zod/src/v3/types.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v3/types.ts), utilizing `in` and `out` schema properties to manage the data flow.
- **Short-circuiting behavior** ensures that failed refinements, exceptions, or dirty flags halt processing before reaching subsequent pipeline stages.
- Both **synchronous** and **asynchronous** transformations are supported through dedicated `_parseSync` and `_parseAsync` paths.
- You can construct pipelines via method chaining (`.transform().pipe()`) or the functional `pipeline()` helper exported from the main types module.

## Frequently Asked Questions

### What is the difference between transform and pipe in Zod?

**Transform** modifies the parsed value and returns a new schema representing the transformed type, while **pipe** creates a `ZodPipeline` that validates the transformed output against a second schema. You use `.transform()` to change data types (like string to number), then `.pipe()` to apply additional validation constraints to that new type. According to the source code in [`packages/zod/src/v3/types.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v3/types.ts), the pipe method instantiates `ZodPipeline.create` to link the transform's output schema with the validation schema.

### How does Zod handle errors in pipeline transformations?

Zod implements **strict short-circuiting** in the `_parse` method of `ZodPipeline` (lines 4835-4845 in [`types.ts`](https://github.com/colinhacks/zod/blob/main/types.ts)). If any stage returns an **aborted** status due to a validation error, the entire pipeline stops immediately and returns the error without processing subsequent stages. Similarly, if a refinement fails or a transform throws an exception, the dirty flag or error propagates upward, preventing the value from reaching the output schema. This ensures that expensive operations (like database lookups in async transforms) only run when preliminary validations pass.

### Can I chain multiple pipe operations in Zod?

Yes, you can create **multi-stage pipelines** by chaining multiple `.pipe()` calls, though each pipe creates a new `ZodPipeline` instance wrapping the previous one. For example: `z.string().transform(Number).pipe(z.number()).pipe(z.number().int())`. However, for complex chains with more than two stages, consider using the low-level `pipeline()` helper or breaking the logic into named schema variables for readability. The implementation in `ZodPipeline.create` (lines 4777-4830) treats each pipe as a binary composition, so deep nesting is technically valid but may impact type inference clarity.

### Is Zod pipeline compatible with async/await?

**Yes**, Zod pipelines fully support asynchronous transformations through the `_parseAsync` path implemented in the `ZodPipeline` class (lines 4835-4845 in [`types.ts`](https://github.com/colinhacks/zod/blob/main/types.ts)). When you use `.transform()` with an async function or `.refine()` with an async validator, the pipeline automatically detects the Promise and switches to async parsing mode. You must use `.parseAsync()` or `.safeParseAsync()` on the final schema to execute the chain. The async branch handles promise resolution before passing values to the output schema, ensuring that asynchronous data fetching (like API calls or database queries) integrates seamlessly with Zod's validation lifecycle.