Zod integration
This API is available since Optique 0.7.0.
The @optique/zod package provides seamless integration with Zod, enabling you to use Zod schemas for validating command-line arguments. This allows you to leverage Zod's powerful validation capabilities and reuse existing schemas across your CLI and application code.
deno add jsr:@optique/zod zodnpm add @optique/zod zodpnpm add @optique/zod zodyarn add @optique/zod zodbun add @optique/zod zodBasic usage
The zod() function creates a value parser from any Zod schema:
import { zod } from "@optique/zod";
import { z } from "zod";
// Email validation
const email = zod(z.string().email(), { placeholder: "" });
// Port number with range validation
const port = zod(z.coerce.number().int().min(1024).max(65535), { placeholder: 1024 });
// Enum choices
const logLevel = zod(z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" });IMPORTANT
The options object is required. In particular, placeholder must be a valid stand-in value of the schema's output type. Optique uses it during deferred prompt resolution, so it does not need to be meaningful user data, but it must be safe for downstream transforms.
String coercion
CLI arguments are always strings, so use z.coerce for non-string types:
// ✅ Correct: Use z.coerce for numbers
const age = zod(z.coerce.number().int().min(0), { placeholder: 0 });
// ❌ Won't work: z.number() expects actual numbers, not strings
const num = zod(z.number(), { placeholder: 0 }); NOTE
Both z.boolean() and z.coerce.boolean() are handled specially: instead of rejecting CLI strings or applying JavaScript truthiness semantics, Optique accepts CLI-friendly literals (true/false, 1/0, yes/no, on/off, case-insensitive).
Transformations
Zod's transformation capabilities work seamlessly with Optique:
// Parse and transform to Date
const startDate = zod(z.string().transform((s) => new Date(s)), { placeholder: new Date(0) });
// Transform to uppercase
const name = zod(z.string().transform((s) => s.toUpperCase()), { placeholder: "" });Async schemas
This API is available since Optique 1.1.0.
Use zodAsync() when a schema depends on async refinements or transforms. The returned value parser is async, so run the containing parser with runAsync() or await run():
import { object } from "@optique/core/constructs";
import { option } from "@optique/core/primitives";
import { runAsync } from "@optique/run";
import { zodAsync } from "@optique/zod";
import { z } from "zod";
const parser = object({
apiKey: option(
"--api-key",
zodAsync(
z.string().refine(isKnownApiKey, "Unknown API key."),
{ placeholder: "" },
),
),
});
const config = await runAsync(parser, {
args: ["--api-key", "live_secret"],
});The synchronous zod() helper remains synchronous and still rejects schemas that require Zod's async parse path. zodAsync() preserves the same metavar inference, choices, suggestions, Boolean literal conversion, formatting, and custom error handling as zod(). Fallback values supplied through bindEnv() or bindConfig() are validated by the same schema before they are accepted.
Async validation may run during fallback resolution and other repeated parser paths, including shell completion requests. Keep remote checks bounded and cached when possible, and prefer enum or literal schemas for completion choices so suggestions stay metadata-driven.
Custom error messages
Customize error messages for better user experience:
const email = zod(z.string().email(), {
placeholder: "",
metavar: "EMAIL",
errors: {
zodError: (error, input) =>
message`Please provide a valid email address, got ${input}.`
}
});Integration with Optique
Zod parsers work seamlessly with all Optique features:
import { object } from "@optique/core/constructs";
import { option, argument } from "@optique/core/primitives";
import { zod } from "@optique/zod";
import { z } from "zod";
const config = object({
email: option("--email", zod(z.string().email(), { placeholder: "" })),
port: option("-p", "--port", zod(z.coerce.number().int().min(1024).max(65535), { placeholder: 1024 })),
logLevel: option("--log-level", zod(z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" })),
startDate: argument(zod(z.string().transform((s) => new Date(s)), { placeholder: new Date(0) })),
});Version compatibility
The @optique/zod package supports both Zod v3 (3.25.0+) and Zod v4 (4.0.0+):
- Zod v3: Uses standard error messages from
error.issues[0].message - Zod v4: Automatically uses
prettifyError()when available for better error formatting
Limitations
zod()remains synchronous: Async Zod features likerefine(async ...)and async transforms requirezodAsync(). The sync helper detects schemas that need async parsing when possible and throws aTypeErrorinstead of silently skipping validation.Boolean parsing in unions: The CLI-friendly boolean parsing (accepting
true/false,1/0,yes/no,on/off) applies only when the entire schema is recognized as a boolean type. For unions that are not recognized as wholly boolean, arm precedence is preserved and parsing follows Zod's native union/coercion behavior.
The Zod integration provides a powerful way to reuse validation logic across your entire application while maintaining full type safety and excellent error messages.