Skip to content

Validation and conditions

Used with string or number properties to render selects:

import type { StringProperty } from "@rebasepro/types";
// Simple: each value mapped to its label
const simple: StringProperty = {
type: "string",
name: "Status",
enum: { draft: "Draft", published: "Published", archived: "Archived" }
};
// As a list, which keeps the order you write
const listed: StringProperty = {
type: "string",
name: "Status",
enum: [
{ id: "draft", label: "Draft" },
{ id: "published", label: "Published" },
{ id: "archived", label: "Archived" }
]
};
// With colors (for Kanban columns and chips)
const colored: StringProperty = {
type: "string",
name: "Status",
enum: [
{ id: "draft", label: "Draft", color: "grayDark" },
{ id: "published", label: "Published", color: "greenDark" },
{ id: "archived", label: "Archived", color: "orangeDark" }
]
};

A plain list of values, enum: ["draft", "published", "archived"], is not part of the type. A JavaScript config that writes one still works: the database enum, the generated types and the admin dropdown all read it as those values, each its own label.

Every rule lives in the property’s validation block, and which rules a property accepts depends on its type — min counts characters on a string, compares magnitudes on a number, and counts elements on an array.

properties: {
title: {
type: "string",
name: "Title",
validation: { required: true, min: 2, max: 200 }
},
slug: {
type: "string",
name: "Slug",
validation: {
unique: true,
matches: /^[a-z0-9-]+$/,
matchesMessage: "Lowercase letters, digits and hyphens only",
trim: true,
lowercase: true
}
},
readingMinutes: {
type: "number",
name: "Reading time",
validation: { integer: true, moreThan: 0, max: 120 }
}
}

The server checks validation at the request boundary — REST, the realtime socket and MCP — on what the caller sent, before beforeSave runs, and answers a write that breaks a rule with a 400 naming every failing field. Server code writing in-process is trusted and is not checked against these rules: rebase.data and rebase.dataAsAdmin in a function or cron job, context.data in a callback, a seed script. What holds there is what the database enforces — required (NOT NULL), unique (a unique index), an enum’s type, and the width of a varchar or char column. Anything else your server code writes, check in the code that writes it.

The admin panel checks the same rules before it saves, in the record form and in the collection table’s inline editor. A table cell judges a value once you edit it: a stored value that fails a rule added later shows no error in the table until someone edits that cell.

Rule Type Description
required boolean The value must be present. On a string, "" counts as absent: the API refuses it on every write, a create or an update, with the same required violation (unless the collection has a beforeSave, which may fill it)
requiredMessage string What is said when it is not: by the admin form, and as the message of the field’s required entry in the API’s 400. The database’s NOT NULL carries no message
unique boolean Only one row in the table may hold this value. The admin form also checks it when saving a value the edit changed, so a duplicate is named on its field before the write
uniqueInArray boolean Only one entry per array may hold it. Applies to a direct child of an array property, or a first-level child of a map inside one. Admin form only — not enforced by the API or the database.
Rule Type Description
min / max number Fewest / most characters, inclusive. With columnType: "varchar" or "char", max also sizes the column (VARCHAR(max)), so lowering it on a table that already holds longer rows is a migration the database refuses, and the collection editor warns when an edit lowers it. The default column is TEXT, which max leaves unbounded — the bound is then the server’s request check alone
length number Exactly this many characters — a country code, a fixed-width reference
matches string | RegExp A pattern the whole value must match. A string is compiled per request, so one that will not compile is rejected at boot rather than quietly becoming no rule
matchesMessage string What the form says when matches fails
trim boolean Strip leading and trailing whitespace before saving. A transform, not a check — it changes what is written, which is what makes it the fix for “the same tag twice, one with a trailing space”. Admin form only — not enforced by the API or the database.
lowercase / uppercase boolean Case-fold the value before saving, like trim. Admin form only — not enforced by the API or the database.

Email and URL formats are not validation rules. email and url are flags on the property itself, beside type — statements about the data that the OpenAPI contract is generated from, rather than form behaviour:

properties: {
email: { type: "string", name: "Email", email: true },
website: { type: "string", name: "Website", url: true }
}

See String properties.

Rule Type Description
min / max number Smallest / largest accepted value, inclusive
moreThan / lessThan number The exclusive twins of min and max
positive / negative boolean Strictly above / below zero. 0 is rejected by both — use min: 0 to allow it
integer boolean No fractional part. Does not change the column type; columnType does
Rule Type Description
min / max Date Earliest / latest accepted date, inclusive. A fixed instant, not “today”
Rule Type Description
min / max number Fewest / most elements, inclusive. Elements, not characters

You can make fields dynamic so they react to the entity’s values. There are two ways to do this:

You can use the conditions property to define declarative JSON Logic rules that can be serialized and modified visually in the collection editor.

price: {
type: "number",
name: "Price",
conditions: {
disabled: { "==": [{ "var": "values.is_free" }, true] },
required: { "!=": [{ "var": "values.is_free" }, true] },
min: 0,
clearOnDisabled: true // Set to null if field gets disabled
}
}

The conditions object gives you access to:

  • disabled, disabledMessage, clearOnDisabled, hidden, readOnly
  • required, requiredMessage, min, max
  • defaultValue
  • enumConditions, allowedEnumValues, excludedEnumValues
  • referencePath, referenceFilter
  • canAddElements, sortable (for arrays)
  • acceptedFiles, maxFileSize (for uploads)

disabledMessage is the sentence a reader sees in place of the control. Without it a greyed-out field explains nothing, which is the difference between a form that is guiding someone and one that is refusing them.

For complex behavior that can’t be expressed via JSON Logic, you can use dynamicProps which evaluates a Javascript function.

price: {
type: "number",
name: "Price",
dynamicProps: ({ values, authController }) => ({
admin: { disabled: values.is_free === true || !authController.user?.roles?.includes("admin") },
validation: values.is_free ? {} : { required: true, min: 0 }
})
}

The same caveat applies, and more sharply: the function is bundled into the panel and called on every render of the form. Admin form only — not enforced by the API or the database.