| title | Troubleshooting & FAQ |
|---|---|
| description | Solutions to common ObjectStack issues — validation failures, query problems, configuration mistakes, and more |
Solutions to the most common issues encountered when working with ObjectStack.
Symptom: Zod validation fails with Invalid option: expected one of "text"|"number"|...
Cause: The type value has a typo or uses an unsupported type name.
Fix: Use exact type names from the Field Type Gallery. Common mistakes:
| ❌ Wrong | ✅ Correct |
|---|---|
text_area |
textarea |
multi_select |
multiselect |
check_boxes |
checkboxes |
master-detail |
master_detail |
auto_number |
autonumber |
rich_text |
richtext |
qr_code |
qrcode |
Symptom: Validation fails with a regex error on the name field.
Cause: Field and object names must match the pattern ^[a-z_][a-z0-9_]*$.
Fix:
| ❌ Wrong | ✅ Correct |
|---|---|
firstName |
first_name |
Order-Item |
order_item |
2ndAddress |
second_address |
STATUS |
status |
Symptom: A select, multiselect, radio, or checkboxes field fails validation.
Cause: Selection-type fields require an options array.
Fix:
// ❌ Missing options
{ name: 'status', type: 'select' }
// ✅ With options
{
name: 'status', type: 'select',
options: [
{ label: 'Open', value: 'open' },
{ label: 'Closed', value: 'closed' }
]
}Symptom: A lookup, master_detail, or tree field fails validation.
Cause: Relational fields require a reference property pointing to the target object.
Fix:
// ❌ Missing reference
{ name: 'owner', type: 'lookup' }
// ✅ With reference
{ name: 'owner', type: 'lookup', reference: 'user' }Possible causes:
- Wrong object name — Object names are snake_case (
project_task, notProjectTask) - Filter typo — Check operator spelling (
$contains, not$contain) - Type mismatch — Comparing string to number or vice versa
- Null handling — Use
$null: trueinstead of$eq: null
Debug steps:
// 1. Start with the simplest query
{ object: 'task', limit: 5 }
// 2. Add fields
{ object: 'task', fields: ['id', 'title', 'status'], limit: 5 }
// 3. Add one filter at a time
{ object: 'task', where: { status: { $eq: 'open' } }, limit: 5 }Symptom: Query fails with invalid_filter error code.
Fix: Check the operator name. Valid operators:
$eq, $ne, $gt, $gte, $lt, $lte,
$in, $nin, $between,
$contains, $notContains, $startsWith, $endsWith,
$null, $exists,
$and, $or, $not
Common mistakes:
| ❌ Wrong | ✅ Correct |
|---|---|
$equal |
$eq |
$notEqual |
$ne |
$like |
$contains |
$greaterThan |
$gt |
$isNull |
$null |
$notIn |
$nin |
Symptom: Query fails with invalid_sort error.
Fix:
- Verify the field exists on the object
- Check that the field has
sortable: true(or is not explicitly set tosortable: false) - Computed fields (formula, summary) may not be sortable
Possible causes:
- Name mismatch — The object name in your query does not match the
namein the definition - Not registered — The object is defined but not included in
defineStack({ objects: [...] }) - Case sensitivity — Object names are exact-match snake_case
Fix:
// Ensure the object is included in the stack (array format)
export default defineStack({
objects: [
{ name: 'project_task', /* ... */ }
]
});
// Or use map format (key becomes the name)
export default defineStack({
objects: {
project_task: { /* ... */ }
}
});
// Use the exact same name when querying
client.data.find('project_task', { /* query */ });Cause: The record has dependent child records via lookup or master_detail fields with deleteBehavior: 'restrict'.
Fix:
- Delete the dependent records first
- Change
deleteBehaviorto'cascade'(deletes children) or'set_null'(clears reference)
// Option A: Cascade delete (children are deleted with parent)
{ name: 'project', type: 'master_detail', reference: 'project', deleteBehavior: 'cascade' }
// Option B: Set null (children keep existing, reference cleared)
{ name: 'project', type: 'lookup', reference: 'project', deleteBehavior: 'set_null' }Symptom: defineStack() in strict mode reports cross-reference errors.
Fix: In strict mode, all references must be valid:
// ❌ View references non-existent object
defineStack({
objects: [{ name: 'task', /* ... */ }],
views: [{ list: { type: 'grid', data: { provider: 'object', object: 'contact' }, columns: ['name'] } }] // 'contact' not defined!
});
// ✅ All references resolve
defineStack({
objects: [
{ name: 'task', /* ... */ },
{ name: 'contact', /* ... */ }
],
views: [{ list: { type: 'grid', data: { provider: 'object', object: 'contact' }, columns: ['name'] } }]
});Cause: Using a raw object literal where a parsed type is expected, or mixing up input vs. output types.
Fix: Use the define* helpers which parse and return the correct types:
// ❌ Raw object — no type checking
const view = { list: { type: 'grid', columns: ['subject', 'status'] } };
// ✅ Parsed with type checking
import { defineView } from '@objectstack/spec';
const view = defineView({
list: {
type: 'grid',
data: { provider: 'object', object: 'task' },
columns: ['subject', 'status', 'priority'],
},
});Cause: Accessing a property that exists on the Zod schema but not on the inferred TypeScript type (usually optional properties that were not provided).
Fix: Use optional chaining or check for undefined:
const field = FieldSchema.parse(data);
// ❌ May be undefined
console.log(field.maxLength.toString());
// ✅ Safe access
console.log(field.maxLength?.toString() ?? 'no limit');Checklist:
- Add indexes — Declare indexes in the object's
indexes[]array (e.g.indexes: [{ fields: ['status'] }]) for fields used inwhereclauses — there is no field-levelindexflag - Limit fields — Only request fields you need (
fields: ['id', 'name']) - Use pagination — Always set
limitto avoid returning all records - Avoid deep nesting — Limit nested
$and/$ordepth - Use cursor pagination — For large datasets, cursor-based is faster than offset
// Optimized query
{
object: 'activity',
fields: ['id', 'type', 'created_at'], // Only needed fields
where: { type: { $eq: 'login' } }, // On indexed field
orderBy: [{ field: 'created_at', order: 'desc' }],
limit: 25,
cursor: { id: lastSeenId } // Keyset/cursor pagination (field → last-seen value)
}Cause: @objectstack/spec does not re-export domain schemas (like FieldSchema or QuerySchema) from its root — only define* factory functions and a handful of top-level schemas are available there. Each domain (~400 Zod schema closures total) is only reachable through its subpath, so an import that assumes it's on the root will not resolve.
Fix: Import from subpaths instead of the root:
// ❌ Not exported from the root at all
import { FieldSchema, QuerySchema } from '@objectstack/spec';
// ✅ Tree-shakeable subpath imports
import { FieldSchema } from '@objectstack/spec/data';
import { ErrorResponseSchema } from '@objectstack/spec/api';Available subpaths: data, api, ui, system, kernel, ai, automation, contracts, integration, security, studio, cloud, qa, identity, shared.
Node.js 22.0.0 or later. The engines field in package.json enforces this.
TypeScript 5.3.0 or later for full type inference support.
Use Zod's .parse() or .safeParse() methods:
import { FieldSchema } from '@objectstack/spec/data';
// Throws on invalid data
const field = FieldSchema.parse(input);
// Returns result object (no throw)
const result = FieldSchema.safeParse(input);
if (result.success) {
console.log(result.data);
} else {
console.error(result.error);
}Use the built-in safeParsePretty() helper:
import { safeParsePretty } from '@objectstack/spec';
const result = safeParsePretty(FieldSchema, input);
// Pretty-printed errors with "Did you mean?" suggestionsYes. All schemas work with plain JavaScript. You lose compile-time type checking but retain runtime validation via Zod's .parse().
Not with .extend() on the schema itself. Several built-in schemas —
FieldSchema, ObjectSchema, ActionSchema — carry a .transform() that lowers
author-facing sugar at parse time, which makes the exported value a ZodPipe,
not a ZodObject. FieldSchema.extend is undefined, so calling it throws
is not a function.
Compose instead: parse with the built-in schema, and validate your additions
alongside it. This keeps the transform running, which .extend() would discard.
{/* os:check */}
import { z } from 'zod';
import { FieldSchema } from '@objectstack/spec/data';
const CustomProps = z.object({ customProperty: z.string().optional() });
function parseCustomField(input: unknown) {
return { ...FieldSchema.parse(input), ...CustomProps.parse(input) };
}
parseCustomField({ name: 'code', type: 'text', customProperty: 'x' });If you genuinely need one merged schema object, FieldSchema.in is the
ZodObject the pipe wraps, so FieldSchema.in.extend({ … }) builds one — but
the result skips the transform, so author-facing sugar the pipe would have
lowered stays raw. Prefer the composition above unless you specifically want the
untransformed shape.
The bundled JSON Schema is at packages/spec/json-schema/objectstack.json (its x-schema-count field reports the total number of definitions). Per-domain schemas are in subfolders under packages/spec/json-schema/ (e.g. data/, ui/, api/).