Overview
The .pipe() method allows you to chain two schemas together, where the output of the first schema becomes the input of the second. This is useful for multi-stage validation and transformation pipelines.
Signature
schema.pipe(targetSchema)
The schema to pipe into. Its input type must match the output type of the source schema.
Returns: ZodPipe<A, B> where A is the source schema and B is the target schema
Basic Usage
String to Number Pipeline
The most common use case is converting and validating data in stages:
const schema = z.string()
.transform(Number)
.pipe(z.number().positive());
schema.parse("42"); // 42
schema.parse("-5"); // Error: Number must be positive
schema.parse("hello"); // Error: Invalid number
Pipes work with async transformations:
const schema = z.string()
.transform(async (val) => Number(val))
.pipe(z.number().int());
await schema.parseAsync("42"); // 42
Advanced Patterns
Multi-Stage Validation
Use pipes to validate at different stages of transformation:
const schema = z.string()
.min(1, "Input cannot be empty")
.transform((val) => val.trim())
.pipe(
z.string()
.email("Must be a valid email")
.transform((email) => email.toLowerCase())
);
schema.parse(" [email protected] "); // "[email protected]"
Conditional Pipelines
Combine pipes with refinements for conditional logic:
const schema = z
.pipe(
z.transform((v) => v === "none" ? undefined : v),
z.string()
)
.catch("default");
schema.parse("ok"); // "ok"
schema.parse("none"); // "default" (transformed to undefined, caught)
schema.parse(15); // "default" (invalid type, caught)
Validate before transforming to prevent invalid transformations:
const schema = z.string()
.refine((c) => c === "1234", "Must be 1234")
.transform((val) => Number(val))
.refine((c) => c === 1234, "Result must be 1234");
schema.parse("1234"); // 1234
const result = schema.safeParse("4321");
// Error at first refinement, transformation never runs
Error Handling
Non-Fatal Errors
By default, refinement errors are non-fatal and allow subsequent checks:
const schema = z.string()
.refine((c) => c.length > 5, "Too short")
.transform((val) => val.toUpperCase())
.refine((c) => c.startsWith("A"), "Must start with A");
const result = schema.safeParse("abc");
// Both refinement errors are reported:
// - "Too short"
// - (transformation runs anyway)
// - "Must start with A"
Fatal Errors
Use abort: true to stop validation on error:
const schema = z.string()
.refine((c) => c === "1234", {
message: "Must be 1234",
abort: true
})
.transform((val) => Number(val))
.refine((c) => c === 1234, "Result must be 1234");
const result = schema.safeParse("4321");
// Only first error reported, transformation and second refinement are skipped
Bidirectional Validation
Encoding and Decoding
Pipes support both forward (decode) and reverse (encode) operations:
const schema = z.string().pipe(z.string());
// Forward: parse/decode
z.decode(schema, "hello"); // "hello"
z.parse(schema, "hello"); // "hello"
// Reverse: encode
z.encode(schema, "hello"); // "hello"
Unidirectional transforms cannot be encoded:
const schema = z.string().transform((val) => val.length);
try {
z.encode(schema, 1234);
} catch (error) {
console.log(error.message);
// "Encountered unidirectional transform during encode: ZodTransform"
}
Type Safety
Pipes maintain type safety throughout the chain:
const schema = z.string()
.transform((s) => s.length)
.pipe(z.number().positive());
type Input = z.input<typeof schema>; // string
type Output = z.output<typeof schema>; // number
// TypeScript enforces correct types
const result: number = schema.parse("hello"); // 5
Type Mismatch Errors
TypeScript will catch type mismatches between piped schemas:
// ❌ Type error: number is not assignable to string
const invalid = z.number()
.pipe(z.string()); // Error!
// ✅ Correct: transform to match next schema's input
const valid = z.number()
.transform(String)
.pipe(z.string());
| Feature | .pipe() | .transform() |
|---|
| Purpose | Chain complete schemas | Transform within schema |
| Validation | Validates at each stage | Validates before transform |
| Reversible | Can be reversible (with codec) | One-way only |
| Type safety | Strict input/output matching | Flexible |
| Use case | Multi-stage pipelines | Single transformation |
// Using pipe (multi-stage)
const piped = z.string()
.transform(Number)
.pipe(
z.number()
.int()
.positive()
);
// Using transform only (single stage)
const transformed = z.string()
.transform((s) => {
const num = Number(s);
if (!Number.isInteger(num) || num <= 0) {
throw new Error("Invalid");
}
return num;
});
Common Patterns
String Preprocessing
Clean and validate string input:
JSON Parsing
Parse and validate JSON strings:
const jsonSchema = z.string()
.transform((str) => JSON.parse(str))
.pipe(
z.object({
id: z.number(),
name: z.string(),
})
);
const data = jsonSchema.parse('{"id": 1, "name": "Alice"}');
// { id: 1, name: "Alice" }
Date Normalization
const dateSchema = z.string()
.transform((s) => new Date(s))
.pipe(
z.date()
.min(new Date("2020-01-01"))
.max(new Date())
);
dateSchema.parse("2023-06-15"); // Date object
const formValueSchema = z.string()
.transform((val) => val === "" ? undefined : val)
.pipe(z.string().min(3).optional());
formValueSchema.parse(""); // undefined
formValueSchema.parse("hello"); // "hello"
Best Practices
Validate early
Place validation as early as possible in the pipeline to catch errors before expensive transformations.
Use meaningful error messages
Add custom error messages at each stage to help identify where validation failed.
Consider performance
Each pipe stage adds overhead. For simple cases, a single transform might be more efficient.
Type safety first
Let TypeScript guide you - if types don’t match, add explicit transforms to bridge the gap.
Error Examples
Type Mismatch
const schema = z.string()
.transform(Number)
.pipe(z.number());
const result = schema.safeParse("not a number");
if (!result.success) {
console.log(result.error.issues);
// [
// {
// code: "invalid_type",
// expected: "number",
// received: "nan",
// path: [],
// message: "Invalid input: expected number, received nan"
// }
// ]
}
Validation Failure
const schema = z.string()
.transform(Number)
.pipe(z.number().int().positive());
const result = schema.safeParse("-5");
if (!result.success) {
console.log(result.error.issues);
// [
// {
// code: "too_small",
// minimum: 0,
// inclusive: false,
// path: [],
// message: "Number must be greater than 0"
// }
// ]
}
See Also