Schema reference

Validation

Built-in validators with custom messages, plus the customValidators escape hatch for plugin checks.

Not verified yet

Every edit the author makes runs the page through the validator pipeline. Errors land in the Errors panel in the right rail with a count badge — both severity: error and severity: warning entries surface there so the author can see what's wrong before they Publish. The validators are advisory today: they don't hard-block Save or Publish, but they're the fastest signal that something's off with the page.

This page covers:

  • The built-in validators — every type-aware check that ships in the box.
  • Per-rule custom error messages so the panel reads as you'd phrase it, not the generic default.
  • The customValidators escape hatch for checks the built-ins don't cover.
  • How visibility (visibleIf) interacts with validation.

Built-in validators

Each built-in attaches to a field via a direct knob — no array wrapping, no plugin registration. The pipeline applies them in a fixed order; the panel groups errors by type.

ValidatorApplies toKnobCustom message
requiredevery fieldrequiredrequiredMessage
minLengthtext, paragraph, linkminLengthminLengthMessage
maxLengthtext, paragraph, linkmaxLengthmaxLengthMessage
regextext, paragraph, linkregexregexMessage
minnumber, rangemin(default)
maxnumber, rangemax(default)
minItemsarray, multichoiceminItemsminItemsMessage
maxItemsarray, multichoicemaxItemsmaxItemsMessage
uniqueOnarrayuniqueOnuniqueOnMessage
shapeevery field(automatic)(default)

required

The most common check. Empty values (empty string, empty array, null, undefined, missing key) are rejected.

- key: title
  type: text
  label: Title
  required: true
  requiredMessage: 'Give the page a title so authors can find it later.'

required on an array means "at least one item." Use minItems: N when you need exactly N or more.

required on a group means "the group must have any value at all" — usually not what you want; require specific child fields instead.

minLength / maxLength

Hard bounds in characters. The editor's counter turns red outside the hard bounds.

- key: description
  type: paragraph
  label: Description
  minLength: 50
  maxLength: 160
  minLengthMessage: "Aim for at least 50 chars so search snippets aren't truncated."

For a soft sweet-spot recommendation (green counter inside the range, amber outside, without surfacing a panel error) use recommendedLength instead:

- key: description
  type: paragraph
  recommendedLength: { min: 50, max: 160 }

regex

JavaScript-flavoured regex source (no surrounding slashes). The validator skips empty values — those are required's domain.

- key: slug
  type: text
  label: Slug
  regex: '^[a-z0-9-]+$'
  regexMessage: 'Lowercase letters, digits, and dashes only.'

For multiple regex checks with different messages, use customValidators — the direct regex: knob allows only one.

min / max

Numeric bounds for number and range. Decimals work — the comparison is JS-numeric.

- key: seats
  type: number
  min: 1
  max: 100

minItems / maxItems

Item-count bounds for array and multichoice. The "Add item" affordance disables at maxItems, so the cap is visible up front.

- key: blocks
  type: array
  minItems: 1
  maxItems: 10
  minItemsMessage: 'Add at least one block — the page needs content.'

uniqueOn

Per-item uniqueness on an array. Path can be a relative dotted path within the item.

- key: faqs
  type: array
  label: FAQs
  uniqueOn: slug
  uniqueOnMessage: 'Two FAQs share the same slug — pick distinct slugs.'
  items:
    - type: group
      key: faq
      label: FAQ
      fields:
        - key: slug
          type: text
          label: Slug
        - key: question
          type: text
          label: Question

Path supports descent: uniqueOn: meta.id checks each item's meta.id value. Items missing the path are skipped (so half-filled drafts don't trigger false positives).

Per-rule custom messages

Every direct validator with a *Message variant lets you replace the default error text. This is the lowest-effort way to make the Errors panel read in the user's voice instead of "Heading is required" / "Description must be at least 50 characters" generics.

- key: heading
  type: text
  required: true
  minLength: 5
  maxLength: 80
  requiredMessage: "Every block needs a heading — it's the first thing users see."
  minLengthMessage: 'Stretch it a bit — under 5 chars reads as a placeholder.'
  maxLengthMessage: 'Keep it tight — over 80 chars wraps awkwardly on mobile.'

Custom messages render verbatim. Skip the field name (the panel already shows it).

customValidators

The escape hatch for checks the built-ins don't cover. Each entry is one validator plus its parameters.

- key: slug
  type: text
  label: Slug
  customValidators:
    - type: regex
      pattern: '^[a-z0-9-]+$'
      message: 'Lowercase letters, digits, and dashes only.'
    - type: expression
      field: $parent.title
      op: nonEmpty
      message: 'Set a title first so we can derive the slug.'

Built-in type: values

typeParamsEffect
regexpattern, optional flagsLike field-level regex: but lets you stack multiple with per-rule messages.
expressionfield, op, valueEvaluate a Condition as a validity check (same path syntax as visibleIf).

Why an array escape hatch (and not more direct fields)

Direct fields stay strongly-typed: minLength is only valid on string-shaped field types; the schema editor + the meta-schema enforce that. customValidators deliberately gives up the per-type narrowing in exchange for extensibility — new validator type:s can land without touching the meta-schema.

The pattern: reach for customValidators when:

  • You need multiple of the same check kind on one field with different messages (e.g. two regex patterns: "lowercase" and "no spaces", each with its own error text).
  • You need a check that depends on another field's value — a "must be filled in once X is set" cross-field rule.
  • The check is plugin-style and might not exist for every site (a remote validator that calls out to your service, a project-specific custom rule).

Use the direct knobs for everything else — they're type-safe and don't need a type: discriminator.

Visibility interaction

Errors are filtered through the visibility set: a field that's hidden by visibleIf doesn't surface errors for itself or its descendants. The filter happens in one place (validatePage in the validation pipeline) so individual validators stay ignorant of visibility — a future validator added to the registry automatically inherits the behaviour.

Why this matters: without the filter, a required field hidden by visibleIf would surface a confusing "is required" error in the Errors panel even though the author can't see the field to fill it in. The current behaviour matches user intuition: "if I can't see it, I can't be expected to fill it in."

The whenHidden: drop option on a field complements this — when the visibility flips false, the stored value is purged on Publish (rather than preserved). Use it when carrying a stale value forward would itself be a data quality problem (e.g. a credit-card field hidden when payment method is invoice).

Validation order

The pipeline runs each validator in registration order, collects errors into one flat array, then applies the visibility filter. The order matters for the panel grouping (which renders errors grouped by type) but not for correctness — every validator runs against every field regardless of what previous validators emitted.

shape runs alongside the others and flags type mismatches (the schema declares type: number but the value on disk is a string, etc). These typically point at content that was authored against an older schema shape and surfaced after a rename.

On this page