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/typeboxBasic 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,notificationsError 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 emailOptional 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
- Use Coercion: For CLI inputs, prefer
z.coercevariants - Provide Descriptions: Help users understand valid values
- Set Sensible Defaults: Use
.default()for optional configs - Validate Early: Catch errors before processing
- 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
- Type Inference - Automatic type inference
- option API - Option helper reference
- Standard Schema - Standard Schema specification