Skip to main content

Validation

Navios Commander uses Zod schemas to validate command options. This ensures type safety and provides helpful error messages when options are invalid.

Basic Validation

Define a Zod schema and pass it to the @Command decorator:

import { Command, CommandHandler } from '@navios/commander'
import { z } from 'zod'

const optionsSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().min(18),
})

@Command({
path: 'create-user',
optionsSchema: optionsSchema,
})
export class CreateUserCommand implements CommandHandler<
z.infer<typeof optionsSchema>
> {
async execute(options) {
// options are validated and typed
console.log('Creating user:', options)
}
}

String Validation

Basic String

const schema = z.object({
name: z.string(),
})

String with Constraints

const schema = z.object({
name: z.string().min(3).max(50),
username: z.string().regex(/^[a-z0-9_]+$/),
email: z.string().email(),
url: z.string().url(),
uuid: z.string().uuid(),
})

String with Custom Validation

const schema = z.object({
password: z.string().min(8).refine(
(password) => /[A-Z]/.test(password),
{ message: 'Password must contain at least one uppercase letter' }
),
})

Number Validation

Basic Number

const schema = z.object({
age: z.number(),
price: z.number(),
})

Number with Constraints

const schema = z.object({
age: z.number().int().min(18).max(120),
price: z.number().positive(),
discount: z.number().min(0).max(100),
})

Number with Precision

const schema = z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180),
})

Boolean Validation

const schema = z.object({
verbose: z.boolean(),
force: z.boolean().default(false),
dryRun: z.boolean().optional(),
})

Array Validation

Basic Array

const schema = z.object({
tags: z.array(z.string()),
ids: z.array(z.number()),
})

Array with Constraints

const schema = z.object({
tags: z.array(z.string()).min(1).max(10),
items: z.array(z.string()).nonempty(),
})

Array of Objects

const schema = z.object({
users: z.array(
z.object({
name: z.string(),
email: z.string().email(),
})
),
})

Object Validation

Nested Objects

const schema = z.object({
user: z.object({
name: z.string(),
email: z.string().email(),
address: z.object({
street: z.string(),
city: z.string(),
zip: z.string(),
}).optional(),
}),
})

Optional Objects

const schema = z.object({
config: z.object({
apiUrl: z.string().url(),
timeout: z.number(),
}).optional(),
})

Optional and Default Values

Optional Fields

const schema = z.object({
name: z.string(),
email: z.string().email().optional(),
age: z.number().optional(),
})

Default Values

const schema = z.object({
name: z.string(),
verbose: z.boolean().default(false),
timeout: z.number().default(5000),
greeting: z.string().default('Hello'),
})

Optional with Default

const schema = z.object({
name: z.string(),
email: z.string().email().optional().default('[email protected]'),
})

Union Types

String Union

const schema = z.object({
role: z.enum(['admin', 'user', 'guest']),
status: z.union([z.literal('active'), z.literal('inactive')]),
})

Type Union

const schema = z.object({
value: z.union([z.string(), z.number()]),
})

Enum Validation

const schema = z.object({
environment: z.enum(['development', 'staging', 'production']),
logLevel: z.enum(['debug', 'info', 'warn', 'error']),
})

Date Validation

const schema = z.object({
startDate: z.string().datetime(),
endDate: z.coerce.date(),
})

Custom Validation

refine()

Add custom validation logic:

const schema = z.object({
password: z.string().min(8).refine(
(password) => {
return /[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password)
},
{ message: 'Password must contain uppercase, lowercase, and number' }
),
})

superRefine()

For complex validation:

const schema = z.object({
startDate: z.string().datetime(),
endDate: z.string().datetime(),
}).superRefine((data, ctx) => {
if (new Date(data.endDate) < new Date(data.startDate)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'End date must be after start date',
})
}
})

Option Descriptions with Meta

Zod v4 supports .meta() to add metadata to schema fields. Navios Commander uses this to display descriptions in help output:

const schema = z.object({
name: z.string().meta({ description: 'User name to display' }),
verbose: z.boolean().default(false).meta({ description: 'Enable verbose output' }),
format: z.enum(['json', 'csv']).meta({ description: 'Output format' }),
})

@Command({
path: 'export',
description: 'Export data to file',
optionsSchema: schema,
})
export class ExportCommand implements CommandHandler<z.infer<typeof schema>> {
async execute(options) {
// ...
}
}

Running export --help will display:

Usage: export [options]

Export data to file

Options:
--name <string> - User name to display
--verbose <boolean> (default: false) - Enable verbose output
--format <enum> - Output format

Meta Placement

The .meta() call should be placed last in the chain for best results:

// ✅ Recommended: meta() at the end
z.string().optional().default('World').meta({ description: 'Name to greet' })

// ✅ Also works: meta() before wrappers (Commander traverses innerType)
z.string().meta({ description: 'Name to greet' }).optional().default('World')

Transform and Preprocess

Transform

Transform values during validation:

const schema = z.object({
port: z.string().transform((val) => parseInt(val, 10)),
tags: z.string().transform((val) => val.split(',')),
})

Preprocess

Preprocess values before validation:

const schema = z.object({
count: z.preprocess(
(val) => (typeof val === 'string' ? parseInt(val, 10) : val),
z.number()
),
})

Error Messages

Zod provides helpful error messages automatically:

const schema = z.object({
email: z.string().email('Invalid email address'),
age: z.number().min(18, 'Must be at least 18 years old'),
})

Custom Error Messages

const schema = z.object({
email: z.string({
required_error: 'Email is required',
invalid_type_error: 'Email must be a string',
}).email('Invalid email format'),
})

Complex Examples

User Creation

const createUserSchema = z.object({
name: z.string().min(3).max(50),
email: z.string().email(),
age: z.number().int().min(18).max(120),
role: z.enum(['admin', 'user', 'guest']).default('user'),
tags: z.array(z.string()).optional(),
metadata: z.object({
department: z.string().optional(),
location: z.string().optional(),
}).optional(),
})

@Command({
path: 'user:create',
optionsSchema: createUserSchema,
})
export class CreateUserCommand implements CommandHandler<
z.infer<typeof createUserSchema>
> {
async execute(options) {
// All options are validated
console.log('Creating user:', options)
}
}

Database Migration

const migrateSchema = z.object({
version: z.string().regex(/^\d+$/),
direction: z.enum(['up', 'down']),
force: z.boolean().default(false),
dryRun: z.boolean().default(false),
})

@Command({
path: 'db:migrate',
optionsSchema: migrateSchema,
})
export class MigrateCommand implements CommandHandler<
z.infer<typeof migrateSchema>
> {
async execute(options) {
// Migration logic
}
}

File Processing

const processFileSchema = z.object({
file: z.string().min(1),
output: z.string().optional(),
format: z.enum(['json', 'csv', 'xml']).default('json'),
options: z.object({
delimiter: z.string().default(','),
encoding: z.string().default('utf-8'),
}).optional(),
})

@Command({
path: 'process:file',
optionsSchema: processFileSchema,
})
export class ProcessFileCommand implements CommandHandler<
z.infer<typeof processFileSchema>
> {
async execute(options) {
// File processing logic
}
}

Validation Errors

When validation fails, Zod throws a ZodError with detailed information:

@Command({ path: 'create-user' })
export class CreateUserCommand implements CommandHandler {
async execute(options) {
// If options don't match schema, ZodError is thrown
// The error includes:
// - Field that failed
// - Expected type
// - Received value
// - Error message
}
}

The application automatically handles validation errors and displays helpful messages:

$ node cli.js create-user --name "John" --email "invalid"
Error: Invalid email format

Best Practices

1. Always Define Schemas

// ✅ Good: Schema defined
@Command({
path: 'create-user',
optionsSchema: z.object({
name: z.string(),
email: z.string().email(),
}),
})

// ❌ Avoid: No validation
@Command({ path: 'create-user' })
export class CreateUserCommand implements CommandHandler {
async execute(options: any) {
// No type safety or validation
}
}

2. Use Descriptive Error Messages

// ✅ Good: Clear error messages
const schema = z.object({
age: z.number().min(18, 'Must be at least 18 years old'),
email: z.string().email('Invalid email address'),
})

3. Provide Defaults for Optional Fields

// ✅ Good: Default values
const schema = z.object({
verbose: z.boolean().default(false),
timeout: z.number().default(5000),
})

4. Use Enums for Limited Choices

// ✅ Good: Enum for limited choices
const schema = z.object({
environment: z.enum(['dev', 'staging', 'prod']),
})

// ❌ Avoid: String with manual validation
const schema = z.object({
environment: z.string(), // No validation
})

Next Steps