| title | Validation |
|---|---|
| description | Validation protocol schemas |
{/*
This module defines the validation schema protocol for ObjectStack, providing a comprehensive
type-safe validation system similar to Salesforce's validation rules but with enhanced capabilities.
Validation rules are applied at the data layer to ensure data integrity and enforce business logic.
A validation rule is a **deterministic, synchronous, side-effect-free predicate over a single
record** — it must be decidable from the incoming write (and, on update, the prior record) with
no I/O. Everything advertised here runs on the write path (see
objectql/src/validation/rule-validator.ts) — insert, single-id update, and multi-row
(multi: true) update, where the evaluator runs once per matched row (#3106); nothing is a
silent no-op. The events enum admits only insert/update for this reason — see the
delete note under "Deliberately NOT validation rules" below.
The system supports these validation types:
-
Script Validation: Formula-based validation using a CEL predicate
-
State Machine Validation: Control allowed state transitions
-
Format Validation: Validate a field's value (email, URL, phone, JSON, regex)
-
Cross-Field Validation: Validate relationships between multiple fields
-
JSON Schema Validation: Validate a JSON field against a JSON Schema
-
Conditional Validation: Apply a nested rule based on a CEL condition
These were once declared here but never enforced. Because the contract above rules them out
(they need I/O or are client-side concerns), they were removed rather than left as silent
no-ops. Use the layer that already does each one correctly:
- Uniqueness → a unique index (
ObjectSchema.indexes,\{ fields, unique: true \},
with partial for a scoped/conditional constraint), or field-level unique: true. A
SELECT-then-INSERT "rule" is inherently racy (TOCTOU); a DB unique constraint is not.
- Async / remote validation → a client-form concern (
debounce/validatorUrlonly mean
anything against keystrokes) and an SSRF/latency hazard on the server write path. Keep it in
the form layer, or enforce the underlying invariant with a unique index / lifecycle hook.
- Custom handler → a
beforeInsert/beforeUpdatelifecycle hook, the typed, supported
extension point for arbitrary validation code.
- Delete-time guards (
events: ['delete']) → abeforeDeletelifecycle hook. The evaluator
only runs on the insert/update write path (a delete carries no record payload to validate), so
a delete event was a proven silent no-op — the enum value was removed rather than left
advertised-but-unenforced (#3184; see docs/audits/2026-06-validationschema-property-liveness.md).
ObjectStack validation rules are inspired by Salesforce validation rules but enhanced:
-
Salesforce: Formula-based validation with
Error Condition Formula -
ObjectStack: Multiple validation types with composable rules
Example Salesforce validation rule:
Rule Name: Discount_Cannot_Exceed_40_Percent
Error Condition Formula: Discount_Percent__c > 0.40
Error Message: Discount cannot exceed 40%.
Equivalent ObjectStack rule:
\{
type: 'script',
name: 'discount_cannot_exceed_40_percent',
condition: 'discount_percent > 0.40',
message: 'Discount cannot exceed 40%',
severity: 'error'
\}import { ConditionalValidation, CrossFieldValidation, FormatValidation, JSONValidation, ScriptValidation, StateMachineValidation, ValidationRule } from '@objectstack/spec/data';
import type { ConditionalValidation, CrossFieldValidation, FormatValidation, JSONValidation, ScriptValidation, StateMachineValidation, ValidationRule } from '@objectstack/spec/data';
// Validate data
const result = ConditionalValidation.parse(data);| Property | Type | Required | Description |
|---|---|---|---|
| name | string |
✅ | Unique rule name (snake_case) |
| label | string |
optional | Human-readable label for the rule listing |
| description | string |
optional | Administrative notes explaining the business reason |
| active | boolean |
optional | |
| events | Enum<'insert' | 'update'>[] |
optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer |
optional | Execution priority (lower runs first, default: 100) |
| tags | string[] |
optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> |
optional | |
| message | string |
✅ | Error message to display to the user |
| type | 'conditional' |
✅ | |
| when | string | { dialect: Enum<'cel' | 'js' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } |
✅ | Predicate (CEL). e.g. Precord.type == 'enterprise' |
| then | { name: string; label?: string; description?: string; active?: boolean; … } | { name: string; label?: string; description?: string; active?: boolean; … } | { name: string; label?: string; description?: string; active?: boolean; … } | { name: string; label?: string; description?: string; active?: boolean; … } | { name: string; label?: string; description?: string; active?: boolean; … } | [ConditionalValidation](#conditionalvalidation) |
✅ | Validation rule to apply when condition is true |
| otherwise | { name: string; label?: string; description?: string; active?: boolean; … } | { name: string; label?: string; description?: string; active?: boolean; … } | { name: string; label?: string; description?: string; active?: boolean; … } | { name: string; label?: string; description?: string; active?: boolean; … } | { name: string; label?: string; description?: string; active?: boolean; … } | [ConditionalValidation](#conditionalvalidation) |
optional | Validation rule to apply when condition is false |
| Property | Type | Required | Description |
|---|---|---|---|
| name | string |
✅ | Unique rule name (snake_case) |
| label | string |
optional | Human-readable label for the rule listing |
| description | string |
optional | Administrative notes explaining the business reason |
| active | boolean |
optional | |
| events | Enum<'insert' | 'update'>[] |
optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer |
optional | Execution priority (lower runs first, default: 100) |
| tags | string[] |
optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> |
optional | |
| message | string |
✅ | Error message to display to the user |
| type | 'cross_field' |
✅ | |
| condition | string | { dialect: Enum<'cel' | 'js' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } |
✅ | Predicate (CEL) comparing fields. e.g. Precord.end_date > record.start_date |
| fields | string[] |
✅ | Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path. |
| Property | Type | Required | Description |
|---|---|---|---|
| name | string |
✅ | Unique rule name (snake_case) |
| label | string |
optional | Human-readable label for the rule listing |
| description | string |
optional | Administrative notes explaining the business reason |
| active | boolean |
✅ | |
| events | Enum<'insert' | 'update'>[] |
✅ | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer |
✅ | Execution priority (lower runs first, default: 100) |
| tags | string[] |
optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> |
✅ | |
| message | string |
✅ | Error message to display to the user |
| type | 'format' |
✅ | |
| field | string |
✅ | |
| regex | string |
optional | |
| format | Enum<'email' | 'url' | 'phone' | 'json'> |
optional |
| Property | Type | Required | Description |
|---|---|---|---|
| name | string |
✅ | Unique rule name (snake_case) |
| label | string |
optional | Human-readable label for the rule listing |
| description | string |
optional | Administrative notes explaining the business reason |
| active | boolean |
✅ | |
| events | Enum<'insert' | 'update'>[] |
✅ | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer |
✅ | Execution priority (lower runs first, default: 100) |
| tags | string[] |
optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> |
✅ | |
| message | string |
✅ | Error message to display to the user |
| type | 'json_schema' |
✅ | |
| field | string |
✅ | JSON field to validate |
| schema | Record<string, any> |
✅ | JSON Schema object definition |
| Property | Type | Required | Description |
|---|---|---|---|
| name | string |
✅ | Unique rule name (snake_case) |
| label | string |
optional | Human-readable label for the rule listing |
| description | string |
optional | Administrative notes explaining the business reason |
| active | boolean |
optional | |
| events | Enum<'insert' | 'update'>[] |
optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer |
optional | Execution priority (lower runs first, default: 100) |
| tags | string[] |
optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> |
optional | |
| message | string |
✅ | Error message to display to the user |
| type | 'script' |
✅ | |
| condition | string | { dialect: Enum<'cel' | 'js' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } |
✅ | Predicate (CEL). If TRUE, validation fails. e.g. Precord.amount < 0 |
| Property | Type | Required | Description |
|---|---|---|---|
| name | string |
✅ | Unique rule name (snake_case) |
| label | string |
optional | Human-readable label for the rule listing |
| description | string |
optional | Administrative notes explaining the business reason |
| active | boolean |
✅ | |
| events | Enum<'insert' | 'update'>[] |
✅ | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer |
✅ | Execution priority (lower runs first, default: 100) |
| tags | string[] |
optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> |
✅ | |
| message | string |
✅ | Error message to display to the user |
| type | 'state_machine' |
✅ | |
| field | string |
✅ | State field (e.g. status) |
| transitions | Record<string, string[]> |
✅ | Map of { OldState: [AllowedNewStates] } |
| initialStates | string[] |
optional | States a record may be CREATED in. When set, an INSERT whose state field carries a value outside this list is rejected (server-enforced) — the FSM entry point. transitions only governs UPDATE, and a select field permits ANY declared option as an initial value, so without this a record could be born mid-flow (e.g. created already approved). Omit to keep the legacy behavior (no initial-state check on insert). #3165. |
This schema accepts one of the following structures:
Type: script
| Property | Type | Required | Description |
|---|---|---|---|
| name | string |
✅ | Unique rule name (snake_case) |
| label | string |
optional | Human-readable label for the rule listing |
| description | string |
optional | Administrative notes explaining the business reason |
| active | boolean |
optional | |
| events | Enum<'insert' | 'update'>[] |
optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer |
optional | Execution priority (lower runs first, default: 100) |
| tags | string[] |
optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> |
optional | |
| message | string |
✅ | Error message to display to the user |
| type | 'script' |
✅ | |
| condition | string | { dialect: Enum<'cel' | 'js' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } |
✅ | Predicate (CEL). If TRUE, validation fails. e.g. Precord.amount < 0 |
Type: state_machine
| Property | Type | Required | Description |
|---|---|---|---|
| name | string |
✅ | Unique rule name (snake_case) |
| label | string |
optional | Human-readable label for the rule listing |
| description | string |
optional | Administrative notes explaining the business reason |
| active | boolean |
optional | |
| events | Enum<'insert' | 'update'>[] |
optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer |
optional | Execution priority (lower runs first, default: 100) |
| tags | string[] |
optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> |
optional | |
| message | string |
✅ | Error message to display to the user |
| type | 'state_machine' |
✅ | |
| field | string |
✅ | State field (e.g. status) |
| transitions | Record<string, string[]> |
✅ | Map of { OldState: [AllowedNewStates] } |
| initialStates | string[] |
optional | States a record may be CREATED in. When set, an INSERT whose state field carries a value outside this list is rejected (server-enforced) — the FSM entry point. transitions only governs UPDATE, and a select field permits ANY declared option as an initial value, so without this a record could be born mid-flow (e.g. created already approved). Omit to keep the legacy behavior (no initial-state check on insert). #3165. |
Type: format
| Property | Type | Required | Description |
|---|---|---|---|
| name | string |
✅ | Unique rule name (snake_case) |
| label | string |
optional | Human-readable label for the rule listing |
| description | string |
optional | Administrative notes explaining the business reason |
| active | boolean |
optional | |
| events | Enum<'insert' | 'update'>[] |
optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer |
optional | Execution priority (lower runs first, default: 100) |
| tags | string[] |
optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> |
optional | |
| message | string |
✅ | Error message to display to the user |
| type | 'format' |
✅ | |
| field | string |
✅ | |
| regex | string |
optional | |
| format | Enum<'email' | 'url' | 'phone' | 'json'> |
optional |
Type: cross_field
| Property | Type | Required | Description |
|---|---|---|---|
| name | string |
✅ | Unique rule name (snake_case) |
| label | string |
optional | Human-readable label for the rule listing |
| description | string |
optional | Administrative notes explaining the business reason |
| active | boolean |
optional | |
| events | Enum<'insert' | 'update'>[] |
optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer |
optional | Execution priority (lower runs first, default: 100) |
| tags | string[] |
optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> |
optional | |
| message | string |
✅ | Error message to display to the user |
| type | 'cross_field' |
✅ | |
| condition | string | { dialect: Enum<'cel' | 'js' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } |
✅ | Predicate (CEL) comparing fields. e.g. Precord.end_date > record.start_date |
| fields | string[] |
✅ | Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path. |
Type: json_schema
| Property | Type | Required | Description |
|---|---|---|---|
| name | string |
✅ | Unique rule name (snake_case) |
| label | string |
optional | Human-readable label for the rule listing |
| description | string |
optional | Administrative notes explaining the business reason |
| active | boolean |
optional | |
| events | Enum<'insert' | 'update'>[] |
optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer |
optional | Execution priority (lower runs first, default: 100) |
| tags | string[] |
optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> |
optional | |
| message | string |
✅ | Error message to display to the user |
| type | 'json_schema' |
✅ | |
| field | string |
✅ | JSON field to validate |
| schema | Record<string, any> |
✅ | JSON Schema object definition |
Type: conditional
| Property | Type | Required | Description |
|---|---|---|---|
| name | string |
✅ | Unique rule name (snake_case) |
| label | string |
optional | Human-readable label for the rule listing |
| description | string |
optional | Administrative notes explaining the business reason |
| active | boolean |
optional | |
| events | Enum<'insert' | 'update'>[] |
optional | Write contexts the rule runs on. delete is intentionally absent — the evaluator only runs on the insert/update write path; guard deletions with a beforeDelete lifecycle hook |
| priority | integer |
optional | Execution priority (lower runs first, default: 100) |
| tags | string[] |
optional | Categorization tags (e.g., "compliance", "billing") |
| severity | Enum<'error' | 'warning' | 'info'> |
optional | |
| message | string |
✅ | Error message to display to the user |
| type | 'conditional' |
✅ | |
| when | string | { dialect: Enum<'cel' | 'js' | 'cron' | 'template'>; source?: string; ast?: any; meta?: object } |
✅ | Predicate (CEL). e.g. Precord.type == 'enterprise' |
| then | [ValidationRule](#validationrule) |
✅ | Validation rule to apply when condition is true |
| otherwise | [ValidationRule](#validationrule) |
optional | Validation rule to apply when condition is false |