ObjectStackObjectStack

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:

PropertyTypeRequiredDescription
namestringUnique rule name (snake_case)
labelstringoptionalDisplay label
descriptionstringoptionalDeveloper documentation
activebooleanIs the rule active
severityenum'error' (blocks save), 'warning' (allows save), 'info'
messagestringUser-facing error message
eventsenum[]When to run: 'insert', 'update', 'delete'
prioritynumberoptionalExecution order (0-9999, lower runs first, default: 100)
tagsstring[]optionalCategorization tags

Severity Levels

SeverityBehavior
errorPrevents the record from being saved
warningShows a warning but allows save
infoInformational 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'],
}
PropertyTypeDescription
fieldsstring[]Fields that must be unique together
scopestringScope field (for per-parent uniqueness)
caseSensitivebooleanCase-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'],
}
PropertyTypeDescription
fieldstringTarget field
regexstringCustom regex pattern
formatenumBuilt-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'],
}
PropertyTypeDescription
fieldstringState field name
transitionsRecord<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'],
}
PropertyTypeDescription
fieldstringField to validate
validatorUrlstringExternal validation endpoint
validatorFunctionstringServer-side function name
timeoutnumberTimeout in milliseconds
debouncenumberDebounce 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,
  },
]

On this page