Using schemas
Any validation library works, as long as it implements Standard Schema and lets primitive types be optional or carry a default value. Zod is used throughout these docs. Valibot, Sury, and Decoders are known to work.
How a value gets built
"3306" the raw terminal string | coerce your coerce function turns it into the expected type v3306 | schema the schema validates it, and applies its own default when nothing was passed voptions.portThat is the whole pipeline. The schema owns the type, the optionality, and the default. coerce only bridges the gap from string.
Primitives
| Vendor | string? | string="value" | coerce |
|---|---|---|---|
| Zod | z.string().optional() | z.string().default("value") | coerce.string |
| Valibot | v.optional(v.string()) | v.optional(v.string(), "value") | coerce.string |
| Decoders | d.optional(d.string) | d.optional(d.string, "value") | coerce.string |
| Sury | S.optional(S.string) | S.optional(S.string, "value") | coerce.string |
Arrays
| Vendor | string[]? | string[]=["value"] | coerce |
|---|---|---|---|
| Zod | z.array(z.string()).optional() | z.array(z.string()).default(["value"]) | coerce.stringArray(",") |
| Valibot | v.optional(v.array(v.string())) | v.optional(v.array(v.string()), ["value"]) | coerce.stringArray(",") |
| Decoders | d.optional(d.array(d.string)) | d.optional(d.array(d.string), ["value"]) | coerce.stringArray(",") |
| Sury | S.optional(S.array(S.string)) | S.optional(S.array(S.string), ["value"]) | coerce.stringArray(",") |
Optional inputs and defaults
Whether an option or a typed argument is required, and what it falls back to, comes from the schema. Nothing in the definition around it changes that.
z.string(); // requiredz.string().optional(); // may be missing, value is undefinedz.string().default("value"); // may be missing, value is "value"meta.optional and meta.default change nothing
Both exist to fix what the help output says. They have no effect at runtime.
defineOptions({ listName: { schema: z.string(), // required, and stays required meta: { optional: true, // help output calls it optional anyway default: `"my-list"`, // help output shows a default it does not have }, },});Reach for them when the real schema would read confusingly in help, not to declare something you wish were true. Setting meta.default to an empty string hides the default from help output entirely.
Where a value came from
The context records how each value was filled in, so you can tell a real input from a default:
cli.onExecute(({ context }) => { const source = context.options.listName.source; // "terminal" | "default" | "programmatic"});Constraints ignore defaults. An option that is only present because of its default does not satisfy requires, and does not trigger conflictWith or exclusive. See Constraints.
Async schemas are not supported
Validation runs synchronously. A schema that only validates asynchronously fails with ValidationErrorCode.AsyncSchemaNotSupported.
This is about the schema itself, not about your handlers, which can be async whenever you use runAsync or executeAsync.
Reading the validation issues
When a schema rejects a value, the error carries the issues that the library reported:
if (result.error?.code === ValidationErrorCode.SchemaValidationFailed) { console.log(result.error.context.issues);}See Errors.