Overview
Codecs provide bidirectional transformation between two representations of data. They combine input validation, output validation, and reversible transformations.
Basic Codec
Create a codec using z.codec() with two schemas and transformation functions:
import * as z from 'zod';
const isoDateCodec = z.codec(
z.iso.datetime(), // Input: ISO string
z.date(), // Output: Date object
{
decode: (isoString) => new Date(isoString), // ISO string → Date
encode: (date) => date.toISOString() // Date → ISO string
}
);
// Forward decoding: ISO string → Date
const date = z.decode(isoDateCodec, "2024-01-15T10:30:00.000Z");
console.log(date); // Date object
// Backward encoding: Date → ISO string
const isoString = z.encode(isoDateCodec, new Date("2024-01-15T10:30:00.000Z"));
console.log(isoString); // "2024-01-15T10:30:00.000Z"
Codec Structure
A codec is defined with:
- Input Schema (A): Validates the encoded form
- Output Schema (B): Validates the decoded form
- decode: Transforms
A → B (forward direction)
- encode: Transforms
B → A (backward direction)
z.codec(
inputSchema, // Schema A
outputSchema, // Schema B
{
decode: (a) => b, // A → B
encode: (b) => a // B → A
}
);
Codec Operations
Decode (Forward)
Transform from input to output representation:
const stringNumberCodec = z.codec(
z.string(),
z.number(),
{
decode: (str) => Number.parseFloat(str),
encode: (num) => num.toString()
}
);
// Synchronous decode
const num = z.decode(stringNumberCodec, "42.5");
console.log(num); // 42.5
// Safe decode (doesn't throw)
const result = z.safeDecode(stringNumberCodec, "invalid");
if (result.success) {
console.log(result.data);
} else {
console.log(result.error);
}
Encode (Backward)
Transform from output back to input representation:
// Synchronous encode
const str = z.encode(stringNumberCodec, 42.5);
console.log(str); // "42.5"
// Safe encode (doesn't throw)
const result = z.safeEncode(stringNumberCodec, 42.5);
if (result.success) {
console.log(result.data); // "42.5"
}
Async Operations
All codec operations support async transformations:
const asyncCodec = z.codec(
z.string(),
z.number(),
{
decode: async (str) => {
await new Promise(resolve => setTimeout(resolve, 1));
return Number.parseFloat(str);
},
encode: async (num) => {
await new Promise(resolve => setTimeout(resolve, 1));
return num.toString();
}
}
);
// Async decode
const decoded = await z.decodeAsync(asyncCodec, "42.5");
console.log(decoded); // 42.5
// Async encode
const encoded = await z.encodeAsync(asyncCodec, 42.5);
console.log(encoded); // "42.5"
// Safe async operations
const safeResult = await z.safeDecodeAsync(asyncCodec, "123");
Round-Trip Conversion
Codecs guarantee bidirectional transformation:
const isoDateCodec = z.codec(
z.iso.datetime(),
z.date(),
{
decode: (isoString) => new Date(isoString),
encode: (date) => date.toISOString()
}
);
const original = "2024-12-25T15:45:30.123Z";
const toDate = z.decode(isoDateCodec, original);
const backToString = z.encode(isoDateCodec, toDate);
console.log(backToString); // "2024-12-25T15:45:30.123Z"
console.log(backToString === original); // true
Codec Type Signatures
The type system ensures correct transformations:
const codec = z.codec(
z.string(),
z.number(),
{
// decode parameter must be: core.output<A> (string)
// decode return must be: core.input<B> (number)
decode: (value: string) => Number(value),
// encode parameter must be: core.input<B> (number)
// encode return must be: core.output<A> (string)
encode: (value: number) => String(value)
}
);
// Type inference works automatically
const decoded: number = z.decode(codec, "123");
const encoded: string = z.encode(codec, 123);
Codecs with Refinements
Add refinements to codec schemas:
const isoDateCodec = z.codec(
z.iso.datetime(),
z.date(),
{
decode: (isoString) => new Date(isoString),
encode: (date) => date.toISOString()
}
).refine((val) => val.getFullYear() === 2024, {
error: "Year must be 2024"
});
// Valid 2024 date
const validDate = z.decode(isoDateCodec, "2024-01-15T10:30:00.000Z");
console.log(validDate.getFullYear()); // 2024
// Invalid year
const invalidResult = z.safeDecode(isoDateCodec, "2023-01-15T10:30:00.000Z");
if (!invalidResult.success) {
console.log(invalidResult.error.issues);
// [{ code: "custom", message: "Year must be 2024", ... }]
}
Complex Codec Example
Nested object with codec property:
const waypointSchema = z.object({
name: z.string().min(1, "Waypoint name required"),
difficulty: z.enum(["easy", "medium", "hard"]),
coordinate: z.codec(
z.string().regex(/^-?\d+,-?\d+$/, "Must be 'x,y' format"),
z.object({ x: z.number(), y: z.number() })
.refine((coord) => coord.x >= 0 && coord.y >= 0, {
error: "Coordinates must be non-negative"
}),
{
decode: (coordString: string) => {
const [x, y] = coordString.split(",").map(Number);
return { x, y };
},
encode: (coord: { x: number; y: number }) => `${coord.x},${coord.y}`
}
).refine((coord) => coord.x <= 1000 && coord.y <= 1000, {
error: "Coordinates must be within bounds"
})
}).refine((waypoint) => {
return waypoint.difficulty !== "hard" || waypoint.coordinate.x >= 100;
}, {
error: "Hard waypoints must be at least 100 units from origin"
});
const inputWaypoint = {
name: "Summit Point",
difficulty: "medium" as const,
coordinate: "150,200"
};
// Decode: coordinate string → coordinate object
const decoded = z.decode(waypointSchema, inputWaypoint);
console.log(decoded);
// {
// name: "Summit Point",
// difficulty: "medium",
// coordinate: { x: 150, y: 200 }
// }
// Encode: coordinate object → coordinate string
const encoded = z.encode(waypointSchema, decoded);
console.log(encoded);
// {
// name: "Summit Point",
// difficulty: "medium",
// coordinate: "150,200"
// }
Validation at Multiple Levels
Codecs validate at each level:
// Input validation (string format)
const result1 = z.safeDecode(waypointSchema, {
name: "Test",
difficulty: "easy",
coordinate: "invalid" // Fails regex validation
});
// Error: "Must be 'x,y' format"
// Output validation (coordinate constraints)
const result2 = z.safeDecode(waypointSchema, {
name: "Test",
difficulty: "easy",
coordinate: "-5,10" // Fails non-negative check
});
// Error: "Coordinates must be non-negative"
// Codec refinement (bounds check)
const result3 = z.safeDecode(waypointSchema, {
name: "Test",
difficulty: "easy",
coordinate: "1500,2000" // Exceeds bounds
});
// Error: "Coordinates must be within bounds"
// Object refinement (hard waypoint constraint)
const result4 = z.safeDecode(waypointSchema, {
name: "Expert Point",
difficulty: "hard",
coordinate: "50,60" // x < 100 for hard difficulty
});
// Error: "Hard waypoints must be at least 100 units from origin"
Mutating Refinements
Codecs support refinements that mutate data:
const A = z.codec(
z.string(),
z.string().trim(),
{
decode: (val) => val,
encode: (val) => val
}
);
console.log(z.decode(A, " asdf ")); // "asdf" (trimmed)
console.log(z.encode(A, " asdf ")); // "asdf" (trimmed)
// With .check() for inline checks
const B = z.codec(
z.string(),
z.string(),
{
decode: (val) => val,
encode: (val) => val
}
).check(z.trim(), z.maxLength(4));
console.log(z.decode(B, " asdf ")); // "asdf"
console.log(z.encode(B, " asdf ")); // "asdf"
Codec with Overwrites
Apply transformations after codec operations:
const stringPlusA = z.string().overwrite((val) => val + "a");
const A = z.codec(
stringPlusA,
stringPlusA,
{
decode: (val) => val,
encode: (val) => val
}
).overwrite((val) => val + "a");
console.log(z.decode(A, "")); // "aaa"
console.log(z.encode(A, "")); // "aaa"
Instance Checks
const codec = z.codec(z.iso.datetime(), z.date(), {
decode: (iso) => new Date(iso),
encode: (date) => date.toISOString()
});
// Codec is also a Pipe and Type
console.log(codec instanceof z.ZodCodec); // true
console.log(codec instanceof z.ZodPipe); // true
console.log(codec instanceof z.ZodType); // true
console.log(codec instanceof z.core.$ZodCodec); // true
Error Handling
const isoDateCodec = z.codec(
z.iso.datetime(),
z.date(),
{
decode: (isoString) => new Date(isoString),
encode: (date) => date.toISOString()
}
);
// Input validation error
const result = z.safeDecode(isoDateCodec, "invalid-date");
if (!result.success) {
console.log(result.error.issues);
// [{
// code: "invalid_format",
// format: "datetime",
// message: "Invalid ISO datetime",
// origin: "string",
// path: [],
// pattern: "/^(?:(?:\\d\\d..."
// }]
}
Best Practices
- Ensure reversibility - Encoding after decoding should return the original value
- Validate both directions - Both input and output schemas should have proper validation
- Use refinements for constraints - Add refinements to enforce additional rules
- Handle edge cases - Consider how transformations handle null, undefined, edge values
- Type safety - Let TypeScript infer types from codec definitions
- Async when needed - Use async transforms for I/O operations
Codecs are perfect for API serialization, database value conversion, and any scenario requiring reversible transformations with validation.
Make sure encode and decode functions are true inverses. Non-reversible codecs can lead to data loss or validation errors.