Validate
Standard Schema-first validation for CLI arguments and flags.
@crustjs/validate validates CLI arguments and flags against any
Standard Schema v1 object — Zod, Effect,
Valibot, ArkType, Sury, or anything else. Use it when you want a Crust
command handler to receive typed, transformed args / flags.
Validation lives where it's used
- Prompts —
@crustjs/promptsaccepts any Standard Schema directly oninput()/password()via itsvalidate:slot. You do not need@crustjs/validatefor prompts. - Store fields —
@crustjs/storeships its ownfield()factory that builds aFieldDeffrom any Standard Schema. You do not need@crustjs/validatefor stores.
Reach for @crustjs/validate only for command-level arg() / flag() /
commandValidator().
Install
bun add @crustjs/validatePick whichever Standard Schema-compatible library you prefer — see the Getting Started tabs below for per-library setup.
Getting Started
The same arg() / flag() / commandValidator() API works with any
Standard Schema v1 library. The tabs below build the same serve command
three different ways so you can compare:
Zod 4 schemas implement Standard Schema natively — pass them straight to
arg() / flag():
bun add zodimport { Crust } from "@crustjs/core";
import { arg, commandValidator, flag } from "@crustjs/validate";
import { z } from "zod";
const serve = new Crust("serve")
.meta({ description: "Start the dev server" })
.args([
arg("port", z.coerce.number().int().min(1), {
description: "Port to listen on",
}),
arg("host", z.string().default("localhost")),
])
.flags({
verbose: flag(z.boolean().default(false), {
type: "boolean",
short: "v",
description: "Enable verbose logging",
}),
})
.run(
commandValidator(({ args, flags }) => {
// args.port: number, args.host: string, flags.verbose: boolean
}),
);Wrap raw Effect schemas once with Schema.standardSchemaV1(...) before
passing them to arg() / flag(). Crust uses only the wrapper's Standard
Schema validate function at runtime.
bun add effectimport { Crust } from "@crustjs/core";
import { arg, commandValidator, flag } from "@crustjs/validate";
import * as Schema from "effect/Schema";
const serve = new Crust("serve")
.meta({ description: "Start the dev server" })
.args([
arg("port", Schema.standardSchemaV1(Schema.NumberFromString), {
description: "Port to listen on",
}),
arg("host", Schema.standardSchemaV1(Schema.String)),
])
.flags({
verbose: flag(Schema.standardSchemaV1(Schema.UndefinedOr(Schema.Boolean)), {
type: "boolean",
short: "v",
description: "Enable verbose logging",
}),
})
.run(
commandValidator(({ args, flags }) => {
// args.port: number, args.host: string, flags.verbose: boolean | undefined
}),
);If the standardSchemaV1(...) wrapping gets noisy, see the
Effect helper recipe below for typed earg /
eflag shorthands.
Valibot schemas implement Standard Schema natively — pass them straight to
arg() / flag():
bun add valibotimport { Crust } from "@crustjs/core";
import { arg, commandValidator, flag } from "@crustjs/validate";
import * as v from "valibot";
const serve = new Crust("serve")
.meta({ description: "Start the dev server" })
.args([
arg("port", v.pipe(v.string(), v.transform(Number), v.integer(), v.minValue(1)), {
description: "Port to listen on",
}),
arg("host", v.optional(v.string(), "localhost")),
])
.flags({
verbose: flag(v.optional(v.boolean(), false), {
type: "boolean",
short: "v",
description: "Enable verbose logging",
}),
})
.run(
commandValidator(({ args, flags }) => {
// args.port: number, args.host: string, flags.verbose: boolean
}),
);Parser grammar lives in Crust options
Descriptions, aliases, and flag type are Crust metadata, not schema metadata. For flags, type
declares CLI grammar/token ownership — boolean flags do not consume a value, while string and
number flags consume --flag value / --flag=value. Schemas decide requiredness, defaults, and
transformations by validating the parsed value afterwards.
For positional args, type is optional because the token is already owned by
the argument; the raw positional string (or string array for variadic args)
flows straight into your schema. Other Standard Schema v1 libraries (ArkType,
Sury, etc.) work the same way as the examples above.
Command validation
commandValidator(handler) returns a run function for the Crust
builder that:
- Reads the Standard Schema attached to each
arg()/flag()definition. - Validates parsed CLI input against every schema (handles sync and
Promise-returning~standard.validatetransparently). - Calls
handlerwith aValidatedContextcontaining the transformed values, or throwsCrustError("VALIDATION")with normalized issues.
Strict mode: every arg/flag must come from this package's arg()
/ flag() helpers. Mixing in a plain core def causes the handler
parameter to resolve to never at compile time.
Helpers
parseValue(schema, value)
Validate any value through any Standard Schema and get the transformed output back, typed:
import { parseValue } from "@crustjs/validate";
import { z } from "zod";
const port = await parseValue(z.coerce.number().int().positive(), "8080");
// port is typed as `number`Throws CrustError("VALIDATION") with all issues in
error.details.issues on failure. Useful for schema-driven parsing
outside the arg() / flag() flow — for example, validating an
environment variable or a value read from a file.
validateStandard / validateStandardSync / isStandardSchema
Low-level primitives for code that needs to handle the result without throwing, or to runtime-check whether an object implements Standard Schema v1:
import { isStandardSchema, validateStandard, validateStandardSync } from "@crustjs/validate";
const result = await validateStandard(schema, value);
if (result.ok) {
// result.value is typed
} else {
// result.issues: { message, path }[]
}
// Sync — throws TypeError if the schema returns a Promise.
const sync = validateStandardSync(schema, value);
if (isStandardSchema(maybe)) {
// maybe is now narrowed to StandardSchema
}Validation errors
All failures normalize to CrustError("VALIDATION") with:
- A bullet-list message rendered from each issue's
pathandmessage. error.details.issues: { path: string; message: string }[]— the raw issues with dot-paths (args[0].port,flags.verbose, …).error.cause— the same array of issues.
See also
- Standard Schema v1 spec
@crustjs/core— the CLI framework itself@crustjs/prompts— schema-driven prompt validation (independent of this package)@crustjs/store— schema-driven store-field validation via its ownfield()factory (independent of this package)