Validation Metadata
Define data integrity rules — formula conditions, uniqueness, format, state machine transitions, and more
Validation Metadata
Validation rules enforce data integrity at the platform level. They run automatically on insert, update, or delete events, preventing invalid data from being saved. ObjectStack supports 9 validation types covering formulas, uniqueness, formats, state transitions, and custom logic.
Basic Structure
Validations are defined on an Object's validations array:
import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Order = ObjectSchema.create({
name: 'order',
label: 'Order',
fields: {
amount: Field.currency({ label: 'Amount', required: true }),
status: Field.select({
label: 'Status',
options: [
{ label: 'Draft', value: 'draft', default: true },
{ label: 'Submitted', value: 'submitted' },
{ label: 'Approved', value: 'approved' },
{ label: 'Cancelled', value: 'cancelled' },
],
}),
email: Field.email({ label: 'Contact Email' }),
},
validations: [
{
name: 'amount_positive',
type: 'script',
severity: 'error',
message: 'Amount must be greater than zero',
condition: 'amount <= 0',
events: ['insert', 'update'],
},
{
name: 'email_unique',
type: 'uniqueness',
severity: 'error',
message: 'Contact email must be unique',
fields: ['email'],
},
],
});Common Properties
All validation types share these base properties:
| Property | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Unique rule name (snake_case) |
label | string | optional | Display label |
description | string | optional | Developer documentation |
active | boolean | ✅ | Is the rule active |
severity | enum | ✅ | 'error' (blocks save), 'warning' (allows save), 'info' |
message | string | ✅ | User-facing error message |
events | enum[] | ✅ | When to run: 'insert', 'update', 'delete' |
priority | number | optional | Execution order (0-9999, lower runs first, default: 100) |
tags | string[] | optional | Categorization tags |
Severity Levels
| Severity | Behavior |
|---|---|
error | Prevents the record from being saved |
warning | Shows a warning but allows save |
info | Informational message, no blocking |
Validation Types
Script Validation
Formula-based validation using expressions:
{
name: 'close_date_required',
type: 'script',
severity: 'error',
message: 'Close date is required for closed deals',
condition: "status = 'closed_won' AND ISBLANK(close_date)",
events: ['insert', 'update'],
}The condition expression should evaluate to true when the data is invalid.
Uniqueness Validation
Ensure field values are unique (single or composite):
// Single field uniqueness
{
name: 'email_unique',
type: 'uniqueness',
severity: 'error',
message: 'Email address must be unique',
fields: ['email'],
caseSensitive: false,
events: ['insert', 'update'],
}
// Composite uniqueness (unique per combination)
{
name: 'code_per_org',
type: 'uniqueness',
severity: 'error',
message: 'Code must be unique within each organization',
fields: ['code', 'organization'],
scope: 'organization',
events: ['insert', 'update'],
}| Property | Type | Description |
|---|---|---|
fields | string[] | Fields that must be unique together |
scope | string | Scope field (for per-parent uniqueness) |
caseSensitive | boolean | Case-sensitive comparison |
Format Validation
Validate against a regex pattern or standard format:
// Regex pattern
{
name: 'phone_format',
type: 'format',
severity: 'error',
message: 'Phone must match format: +1-XXX-XXX-XXXX',
field: 'phone',
regex: '^\\+1-\\d{3}-\\d{3}-\\d{4}$',
events: ['insert', 'update'],
}
// Built-in format
{
name: 'website_format',
type: 'format',
severity: 'error',
message: 'Website must be a valid URL',
field: 'website',
format: 'url',
events: ['insert', 'update'],
}| Property | Type | Description |
|---|---|---|
field | string | Target field |
regex | string | Custom regex pattern |
format | enum | Built-in format: 'email', 'url', 'phone', 'json' |
State Machine Validation
Enforce allowed state transitions:
{
name: 'order_status_transitions',
type: 'state_machine',
severity: 'error',
message: 'Invalid status transition',
field: 'status',
transitions: {
draft: ['submitted', 'cancelled'],
submitted: ['approved', 'rejected', 'cancelled'],
approved: ['completed'],
rejected: ['draft'],
cancelled: [],
completed: [],
},
events: ['update'],
}| Property | Type | Description |
|---|---|---|
field | string | State field name |
transitions | Record<string, string[]> | Map of current state → allowed next states |
Cross-Field Validation
Validate relationships between multiple fields:
{
name: 'date_range_valid',
type: 'cross_field',
severity: 'error',
message: 'End date must be after start date',
fields: ['start_date', 'end_date'],
condition: 'end_date <= start_date',
events: ['insert', 'update'],
}JSON Schema Validation
Validate JSON data against a JSON Schema:
{
name: 'config_schema',
type: 'json',
severity: 'error',
message: 'Configuration JSON is invalid',
field: 'config',
schema: {
type: 'object',
required: ['host', 'port'],
properties: {
host: { type: 'string' },
port: { type: 'number', minimum: 1, maximum: 65535 },
},
},
events: ['insert', 'update'],
}Async Validation
Validate against an external service:
{
name: 'tax_id_valid',
type: 'async',
severity: 'error',
message: 'Invalid tax ID',
field: 'tax_id',
validatorUrl: 'https://api.validation-service.com/verify',
timeout: 5000,
debounce: 300,
events: ['insert', 'update'],
}| Property | Type | Description |
|---|---|---|
field | string | Field to validate |
validatorUrl | string | External validation endpoint |
validatorFunction | string | Server-side function name |
timeout | number | Timeout in milliseconds |
debounce | number | Debounce delay in milliseconds |
Conditional Validation
Apply validation only when a condition is met:
{
name: 'billing_required_for_paid',
type: 'conditional',
when: "plan_type = 'paid'",
then: {
name: 'billing_address_check',
type: 'script',
severity: 'error',
message: 'Billing address is required for paid plans',
condition: 'ISBLANK(billing_address)',
events: ['insert', 'update'],
},
events: ['insert', 'update'],
}Custom Validator
Invoke a named server-side handler:
{
name: 'inventory_check',
type: 'custom',
severity: 'error',
message: 'Insufficient inventory',
handler: 'checkInventoryAvailability',
params: { warehouse: 'primary' },
events: ['insert', 'update'],
}Priority
When multiple validations exist, priority controls execution order:
// Runs first (lower number = higher priority)
{ name: 'format_check', priority: 10, ... }
// Runs second
{ name: 'uniqueness_check', priority: 50, ... }
// Runs last (default priority)
{ name: 'business_rule', priority: 100, ... }Complete Example
validations: [
// Formula validation
{
name: 'amount_positive',
type: 'script',
severity: 'error',
message: 'Order amount must be positive',
condition: 'amount <= 0',
events: ['insert', 'update'],
priority: 10,
},
// State transition
{
name: 'status_flow',
type: 'state_machine',
severity: 'error',
message: 'Invalid status transition',
field: 'status',
transitions: {
draft: ['submitted'],
submitted: ['approved', 'rejected'],
approved: ['shipped'],
rejected: ['draft'],
shipped: ['delivered'],
},
events: ['update'],
priority: 20,
},
// Uniqueness
{
name: 'order_number_unique',
type: 'uniqueness',
severity: 'error',
message: 'Order number must be unique',
fields: ['order_number'],
events: ['insert', 'update'],
priority: 30,
},
// Cross-field
{
name: 'shipping_date_after_order',
type: 'cross_field',
severity: 'error',
message: 'Ship date must be after order date',
fields: ['order_date', 'ship_date'],
condition: 'ship_date < order_date',
events: ['insert', 'update'],
priority: 40,
},
]Related
- Object Metadata — Objects contain validation rules
- Field Metadata — Field-level constraints (
required,unique,min,max) - Workflow Metadata — Event-driven business automation