Comprehensive guide for implementing validation rules in ObjectStack.
| Type | Purpose | When Validation Fails |
|---|---|---|
script |
Formula expression | When expression evaluates to true |
unique |
Composite uniqueness | When duplicate found |
state_machine |
Legal state transitions | When transition not allowed |
format |
Regex or built-in format | When format doesn't match |
cross_field |
Compare values across fields | When comparison fails |
json_schema |
Validate JSON field | When JSON doesn't match schema |
async |
External API validation | When API returns error |
custom |
Registered validator function | When function returns false |
conditional |
Apply rule conditionally | When nested rule fails |
true.
validations: [
{
name: 'prevent_past_dates',
type: 'script',
condition: 'due_date < TODAY()', // ❌ Fails when this is TRUE
message: 'Due date cannot be in the past',
severity: 'error',
events: ['insert', 'update'],
},
]// Prevent negative values
condition: 'amount < 0'
// Require field when another field has value
condition: 'status = "approved" AND approver_id IS NULL'
// Date range validation
condition: 'end_date < start_date'
// Conditional required field
condition: 'type = "enterprise" AND account_manager IS NULL'validations: [
{
name: 'unique_email',
type: 'unique',
fields: ['email'],
caseSensitive: false,
message: 'Email address already exists',
},
{
name: 'unique_tenant_email',
type: 'unique',
fields: ['tenant_id', 'email'], // Composite uniqueness
caseSensitive: false,
message: 'Email already exists in this tenant',
},
]validations: [
{
name: 'status_flow',
type: 'state_machine',
field: 'status',
transitions: {
draft: ['submitted', 'cancelled'],
submitted: ['in_review', 'cancelled'],
in_review: ['approved', 'rejected'],
approved: ['published'],
rejected: ['draft'],
published: [], // Terminal state
cancelled: [], // Terminal state
},
message: 'Invalid status transition',
severity: 'error',
},
]validations: [
// Built-in formats
{
name: 'email_format',
type: 'format',
field: 'email',
format: 'email', // Built-in: email, url, phone, json, uuid
message: 'Invalid email format',
},
// Custom regex
{
name: 'sku_format',
type: 'format',
field: 'sku',
pattern: '^[A-Z]{3}-\\d{4}$', // e.g., ABC-1234
message: 'SKU must be format: XXX-0000',
},
]validations: [
{
name: 'date_range',
type: 'cross_field',
condition: 'end_date > start_date',
message: 'End date must be after start date',
fields: ['start_date', 'end_date'],
},
{
name: 'discount_limit',
type: 'cross_field',
condition: 'discount_amount <= subtotal * 0.5',
message: 'Discount cannot exceed 50% of subtotal',
fields: ['discount_amount', 'subtotal'],
},
]validations: [
{
name: 'config_schema',
type: 'json_schema',
field: 'config',
schema: {
type: 'object',
properties: {
timeout: { type: 'number', minimum: 0 },
retries: { type: 'integer', minimum: 1, maximum: 5 },
enabled: { type: 'boolean' },
},
required: ['timeout', 'enabled'],
additionalProperties: false,
},
message: 'Invalid configuration format',
},
]validations: [
{
name: 'external_api_check',
type: 'async',
field: 'tax_id',
endpoint: 'https://api.example.com/validate/tax-id',
method: 'POST',
timeout: 5000,
debounce: 500, // Delay validation by 500ms
message: 'Invalid tax ID',
},
]validations: [
{
name: 'enterprise_requires_manager',
type: 'conditional',
condition: "type = 'enterprise'",
validations: [
{
name: 'manager_required',
type: 'script',
condition: 'account_manager IS NULL',
message: 'Enterprise accounts must have an account manager',
},
],
},
]severity: 'error' // Blocks save (default)
severity: 'warning' // Allows save, shows warning
severity: 'info' // Informational onlyevents: ['insert'] // Only on create
events: ['update'] // Only on update
events: ['insert', 'update'] // On create and update (default)
events: ['delete'] // Only on deletepriority: 0 // System validations (run first)
priority: 100 // Application validations (default)
priority: 1000 // User validations (run last)Lower numbers execute first.
{
type: 'script',
condition: 'amount > 0', // ❌ Fails when amount > 0 (inverted!)
message: 'Amount must be positive',
}{
type: 'script',
condition: 'amount <= 0', // ✅ Fails when amount <= 0
message: 'Amount must be positive',
}{
type: 'script',
condition: 'end_date < start_date',
message: 'End date must be after start date',
// ❌ No severity — defaults to 'error' which may be too strict
}{
type: 'script',
condition: 'end_date < start_date',
message: 'End date must be after start date',
severity: 'warning', // ✅ Allow save but warn user
}{
type: 'script',
condition: 'status = "draft"',
message: 'Record is still in draft',
// ❌ No events — runs on all operations
}{
type: 'script',
condition: 'status = "draft"',
message: 'Cannot publish draft records',
events: ['update'], // ✅ Only validate on update
}{
name: 'no_backdate',
type: 'script',
condition: 'created_at < TODAY()',
message: 'Cannot create records with past dates',
events: ['insert'],
}{
name: 'high_value_approval',
type: 'conditional',
condition: 'amount > 10000',
validations: [
{
type: 'script',
condition: 'approved_by IS NULL',
message: 'High-value transactions require approval',
},
],
}{
name: 'email_domain',
type: 'format',
field: 'email',
pattern: '^[a-zA-Z0-9._%+-]+@(company\\.com|partner\\.com)$',
message: 'Email must be from company.com or partner.com',
}{
name: 'phone_format',
type: 'format',
field: 'phone',
pattern: '^\\+?[1-9]\\d{1,14}$', // E.164 format
message: 'Phone must be in international format (+1234567890)',
}{
name: 'tenant_email_unique',
type: 'unique',
fields: ['tenant_id', 'email'],
caseSensitive: false,
message: 'Email already exists in this tenant',
}- Use declarative validation first — Only use script validation when declarative rules don't fit
- Severity matters — Use
warningfor soft rules,errorfor hard rules - Events scope — Only validate on relevant operations to avoid overhead
- Priority order — System validations first (0-99), app validations second (100-999), user validations last (1000+)
- Clear error messages — Tell users exactly what's wrong and how to fix it
- Async validation debounce — Use debounce to reduce API calls on fast typing
- State machine for workflows — Use state_machine instead of complex script logic
- Unique constraints — Always use unique validation, not script-based checks
- Cross-field for comparisons — More efficient than script validation
- Test thoroughly — Validate edge cases, nulls, empty strings
- Script validations are expensive — Use sparingly, prefer declarative rules
- Async validations add latency — Use debounce and appropriate timeouts
- Priority affects order — Lower priority = runs first
- Unique checks hit database — Index the unique fields for performance
- State machine is optimized — Better than complex conditional logic