Crust logoCrust

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/prompts accepts any Standard Schema directly on input() / password() via its validate: slot. You do not need @crustjs/validate for prompts.
  • Store fields@crustjs/store ships its own field() factory that builds a FieldDef from any Standard Schema. You do not need @crustjs/validate for stores.

Reach for @crustjs/validate only for command-level arg() / flag() / commandValidator().

Install

bun add @crustjs/validate

Pick 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 zod
import { 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 effect
import { 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 valibot
import { 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:

  1. Reads the Standard Schema attached to each arg() / flag() definition.
  2. Validates parsed CLI input against every schema (handles sync and Promise-returning ~standard.validate transparently).
  3. Calls handler with a ValidatedContext containing the transformed values, or throws CrustError("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 path and message.
  • 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

On this page