Skip to content

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
v
3306
| schema the schema validates it, and applies its own default when nothing was passed
v
options.port

That 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(); // required
z.string().optional(); // may be missing, value is undefined
z.string().default("value"); // may be missing, value is "value"

meta.optional and meta.default change nothing

Important

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

Important

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.