Reliverse Docs
LibsIntroductionCore Concepts

Validation

Schema validation with Standard Schema in Rempts

Validation

Rempts uses Standard Schema for validation, allowing you to use any compatible validation library.

Standard Schema Support

Standard Schema is a specification that provides a common interface for validation libraries. Rempts supports any library that implements this standard:

  • Zod - Popular TypeScript-first schema validation
  • Valibot - Modular and lightweight validation
  • TypeBox - JSON Schema Type Builder
  • Arktype - TypeScript's type syntax at runtime
  • And many more...

Installing validation libraries: Add your preferred validation library to your project:

# For Zod
bun add zod

# For Arktype
bun add arktype

# For Valibot
bun add valibot

# For TypeBox
bun add @sinclair/typebox

Basic Validation

Choose any Standard Schema-compatible validation library:

import { defineCommand, option } from '@reliverse/rempts-core'
import { z } from 'zod'

export default defineCommand({
  name: 'deploy',
  options: {
    port: option(
      z.number()
        .int()
        .min(1)
        .max(65535),
      { description: 'Server port' }
    ),
    email: option(
      z.string().email(),
      { description: 'Contact email' }
    )
  },
  handler: async ({ flags }) => {
    // flags.port is guaranteed to be 1-65535
    // flags.email is guaranteed to be valid email
  }
})
import { defineCommand, option } from '@reliverse/rempts-core'
import { type } from 'arktype'

export default defineCommand({
  name: 'deploy',
  options: {
    port: option(
      type('number.integer > 0 & <= 65535'),
      { description: 'Server port' }
    ),
    email: option(
      type('string.email'),
      { description: 'Contact email' }
    )
  },
  handler: async ({ flags }) => {
    // flags.port is guaranteed to be 1-65535
    // flags.email is guaranteed to be valid email
  }
})
import { defineCommand, option } from '@reliverse/rempts-core'
import * as v from 'valibot'

export default defineCommand({
  name: 'deploy',
  options: {
    port: option(
      v.number([v.integer(), v.minValue(1), v.maxValue(65535)]),
      { description: 'Server port' }
    ),
    email: option(
      v.string([v.email()]),
      { description: 'Contact email' }
    )
  },
  handler: async ({ flags }) => {
    // flags.port is guaranteed to be 1-65535
    // flags.email is guaranteed to be valid email
  }
})

Validation Libraries

Using Zod

import { z } from 'zod'

export default defineCommand({
  options: {
    // Basic types
    name: option(z.string()),
    age: option(z.number()),
    active: option(z.boolean()),
    
    // With constraints
    username: option(
      z.string()
        .min(3)
        .max(20)
        .regex(/^[a-zA-Z0-9_]+$/)
    ),
    
    // Optional with default
    timeout: option(
      z.number().default(30)
    ),
    
    // Complex types
    config: option(
      z.object({
        host: z.string(),
        port: z.number(),
        secure: z.boolean()
      })
    )
  }
})

Using Valibot

import * as v from 'valibot'

export default defineCommand({
  options: {
    // Basic validation
    name: option(v.string()),
    count: option(v.number([
      v.minValue(0),
      v.maxValue(100)
    ])),
    
    // Email validation
    email: option(v.string([v.email()])),
    
    // Custom validation
    password: option(
      v.string([
        v.minLength(8),
        v.regex(/[A-Z]/),
        v.regex(/[0-9]/)
      ])
    )
  }
})

Using TypeBox

import { Type } from '@sinclair/typebox'

export default defineCommand({
  options: {
    // JSON Schema compatible
    name: option(Type.String()),
    port: option(Type.Number({ 
      minimum: 1, 
      maximum: 65535 
    })),
    
    // Complex schema
    server: option(Type.Object({
      host: Type.String(),
      port: Type.Number(),
      ssl: Type.Optional(Type.Boolean())
    }))
  }
})

Using Arktype

Arktype uses TypeScript syntax for runtime type validation:

import { type } from 'arktype'

export default defineCommand({
  options: {
    // Basic types with TypeScript syntax
    name: option(type('string')),
    age: option(type('number')),
    active: option(type('boolean')),

    // String constraints
    username: option(type('string').pipe(s => s.length >= 3 && s.length <= 20 && /^[a-zA-Z0-9_]+$/.test(s))),

    // Union types
    env: option(type("'dev'|'staging'|'prod'")),

    // Optional with defaults
    timeout: option(type('number').pipe(n => n ?? 30)),

    // Complex types
    config: option(type({
      host: 'string',
      port: 'number',
      secure: 'boolean?'
    })),

    // Arrays
    tags: option(type('string[]')),
    ports: option(type('number[]').pipe(ports => ports.every(p => p > 0 && p <= 65535)))
  }
})

Arktype excels at:

  • TypeScript-like syntax - Write types using familiar TypeScript syntax
  • Performance - Fast validation with excellent type inference
  • Complex types - Support for unions, intersections, and complex object schemas
  • Runtime safety - Full type safety at runtime with TypeScript-like errors

Rempts automatically handles string-to-type coercion for command-line inputs:

import { z } from 'zod'

export default defineCommand({
  options: {
    // String inputs are coerced to numbers
    port: option(z.coerce.number()),

    // String "true"/"false" coerced to boolean
    verbose: option(z.coerce.boolean()),

    // String dates coerced to Date objects
    since: option(z.coerce.date())
  },
  handler: async ({ flags }) => {
    // Types are properly coerced:
    // --port 3000 → flags.port is number 3000
    // --verbose true → flags.verbose is boolean true
    // --since 2024-01-01 → flags.since is Date object
  }
})
import { type } from 'arktype'

export default defineCommand({
  options: {
    // String inputs are coerced to numbers
    port: option(type('string.numeric.parse')),

    // String "true"/"false" coerced to boolean
    verbose: option(type('string.boolean.parse')),

    // Date parsing (requires custom coercion)
    since: option(type('string').pipe(s => new Date(s)))
  },
  handler: async ({ flags }) => {
    // Types are properly coerced:
    // --port 3000 → flags.port is number 3000
    // --verbose true → flags.verbose is boolean true
    // --since 2024-01-01 → flags.since is Date object
  }
})
import * as v from 'valibot'

export default defineCommand({
  options: {
    // String inputs are coerced to numbers
    port: option(v.coerce(v.number(), Number)),

    // String "true"/"false" coerced to boolean
    verbose: option(v.coerce(v.boolean(), v.toBoolean())),

    // Date parsing
    since: option(v.coerce(v.date(), (input) => new Date(input)))
  },
  handler: async ({ flags }) => {
    // Types are properly coerced:
    // --port 3000 → flags.port is number 3000
    // --verbose true → flags.verbose is boolean true
    // --since 2024-01-01 → flags.since is Date object
  }
})

Custom Validation

You can add custom validation logic:

const portSchema = z.number().refine(
  (port) => !isPortInUse(port),
  (port) => ({ message: `Port ${port} is already in use` })
)

export default defineCommand({
  options: {
    port: option(portSchema)
  }
})

Array and Multiple Values

Handle multiple values for an option:

export default defineCommand({
  options: {
    // Accept multiple tags
    tags: option(
      z.array(z.string()),
      { description: 'Tags (can be specified multiple times)' }
    ),
    
    // Comma-separated values
    features: option(
      z.string().transform(val => val.split(','))
    )
  }
})

// Usage:
// mycli --tags ui --tags backend --tags api
// mycli --features auth,payments,notifications

Error Messages

Validation errors are automatically formatted and displayed:

$ mycli deploy --port 70000 --email invalid

Validation errors:
  --port:
 Number must be less than or equal to 65535
  --email:
 Invalid email

Optional vs Required

Control whether options are required:

export default defineCommand({
  options: {
    // Required option
    name: option(z.string()),
    
    // Optional option
    description: option(z.string().optional()),
    
    // Optional with default
    port: option(z.number().default(3000)),
    
    // Nullable option
    config: option(z.string().nullable())
  }
})

Complex Validation Scenarios

Dependent Validation

const schema = z.object({
  mode: z.enum(['dev', 'prod']),
  debugPort: z.number().optional()
}).refine(
  data => data.mode !== 'prod' || !data.debugPort,
  { message: 'Debug port cannot be used in production mode' }
)

Union Types

export default defineCommand({
  options: {
    output: option(
      z.union([
        z.literal('json'),
        z.literal('yaml'),
        z.literal('table'),
        z.string().regex(/^custom:/)
      ])
    )
  }
})

Transform and Preprocess

export default defineCommand({
  options: {
    // Parse JSON input
    data: option(
      z.string().transform(str => JSON.parse(str))
    ),
    
    // Normalize paths
    file: option(
      z.string().transform(path => resolve(path))
    ),
    
    // Parse environment variables
    env: option(
      z.string()
        .transform(str => str.split(','))
        .pipe(z.array(z.enum(['dev', 'test', 'prod'])))
    )
  }
})

Best Practices

  1. Use Coercion: For CLI inputs, prefer z.coerce variants
  2. Provide Descriptions: Help users understand valid values
  3. Set Sensible Defaults: Use .default() for optional configs
  4. Validate Early: Catch errors before processing
  5. Custom Error Messages: Provide helpful validation messages

Standard Schema Adapters

If your validation library doesn't natively support Standard Schema, you can use adapters:

import { toStandardSchema } from '@standard-schema/zod'

const schema = toStandardSchema(z.string().email())

See Also

On this page