Validation
When a document is checked (save and publish), required fields and required_in, visible_when and required_when, the expression language, cross-field validation rules with severity and timing, and the publish checks.
Every document is checked against its type on each save and on each publish. A problem is an error, which stops the action, or a warning, which is shown as advice and stops nothing. Each problem has a path (stages[2].title, meta.description) so the admin, the content API and the MCP point at the exact control, inside collapsed repeater items too.
Save and publish
A draft must be able to stop mid-work, so a save checks less than a publish.
| Check | On save | On publish |
|---|---|---|
| Each field type’s own rules (a number is a number, a URL parses, a phone is valid) | error | error |
required, required_in, required_when | not checked | error |
A repeater’s or blocks field’s min, when it has fewer items but not none | error | error |
A repeater’s or blocks field’s min, when it is empty | not checked | error |
unique | warning | error |
Validation rules without on: | as their severity | as their severity |
Validation rules with on: publish | not run | as their severity |
| A link’s target type and site, an asset’s kind and constraints | error | error |
| Asset rights, alt text and missing assets (Asset) | error or warning | |
| A tag relation names a vocabulary tag, not a candidate | error | |
| A nesting document is not placed under itself | error | |
| A relation’s target is not published in this locale or its fallbacks | warning | |
The reserved slug __home__ where it cannot apply | error | error |
Publishing in one locale checks that locale’s view of the document.
Required fields
fields:
- { name: title, type: text, required: true }
- { name: summary, type: text, required: true, required_in: all }
- { name: legalNotice, type: portable_text, required: true, required_in: [en-GB, cy-GB] } required: true makes a field necessary to publish. required_in says in which locales, and applies only with required: true:
required_in | The field is required when publishing |
|---|---|
left out, or source | in the source locale only (the default) |
all | in every locale |
| a locale code or a list | in the source locale if source is listed, and in each locale listed |
The default is deliberate: requiring every locale blocks publishing until everything is translated.
A value is empty when it is missing, a string of only spaces, an empty list, or an empty object. false is a value, so required on a boolean does not force it to be true. The message is This field is required with code required.
Conditional fields
fields:
- { name: bookingRequired, type: boolean }
- name: bookingUrl
type: link
kinds: [{ of: url }]
visible_when: "bookingRequired == true"
required_when: "bookingRequired == true" | Key | What it does |
|---|---|
visible_when | The editor shows the field only while the expression is true, with no round trip to the server. On every save the server evaluates it too, and a hidden field’s value is removed: a crafted post cannot keep a value for a field that should not be there. |
required_when | The field is required to publish while the expression is true. |
They are separate on purpose: a field can be visible and optional, or required by a condition while visible for another. Both read sibling fields only: a top-level field reads other top-level fields, and a field inside a repeater item or a block reads the other fields of that item.
The expression language
visible_when, required_when and validation rules share one small language, parsed and type-checked when the configuration loads.
| Form | Example |
|---|---|
| Compare a field with a literal | tier == 'gold', capacity > 50, dogFriendly == true |
| Compare two fields | endDate >= startDate |
| Emptiness | heroImage == empty, accessibility != empty |
| Membership | difficulty in ['moderate', 'hard'], kind not in ['online'] |
| Length (rules) | summary|length >= 60 |
| Combine | and, or, not, and parentheses: not (tier in ['gold', 'platinum']) or heroImage != empty |
- Operators:
==,!=,<,>,<=,>=,in,not in. - Literals: strings in single or double quotes, numbers,
true,false, andempty(with==and!=only). A list afterinholds literals only. |lengthis the only filter: a string’s length in characters, or a list’s length. It compares with a number.- No function calls, no arithmetic, no paths into other documents.
engine-dmo check type-checks every expression once all schema files are read:
| Message | Cause |
|---|---|
unknown field "x" (with a suggestion) | The name is not a sibling. |
"tlak" is not an option of kind (with the options listed) | A select compared with a value it does not offer. |
price is a number and is compared with a number, not "ten" | The literal does not suit the field: a number needs a number, a boolean true or false, a text, slug, phone, date or datetime a quoted string. |
summary|length compares with a number | A length compared with something other than a number. |
heroImage is a asset field and can only be tested with == empty or != empty | Structured fields (asset, link, video, geo, geo_shape, address, opening_hours, recurrence, seo, portable_text, repeater, blocks) are only tested for emptiness. |
x (number) is compared with y (text), which never match | Two fields of different kinds. |
empty only works with == and !=, unterminated string, expected a list after in | Syntax. |
Comparisons between two values are numeric when both are numbers and textual otherwise. Two date fields compare correctly as text (YYYY-MM-DD). A datetime is a structured value, so compare datetime fields only with == empty or != empty, and use date fields where a rule must order two moments.
Validation rules
Rules check several fields together. They sit in the type’s validation: list:
validation:
- rule: "endDate == empty or endDate >= startDate"
message: { en-GB: The end date must be on or after the start date. }
fields: [endDate]
- rule: "isAccessible == false or accessibility != empty"
message: { en-GB: Describe the surfaces and gradients before marking the walk accessible. }
fields: [accessibility]
on: publish
- rule: "summary == empty or summary|length >= 60"
message: { en-GB: A summary under 60 characters reads poorly in search results. }
fields: [summary]
severity: warning | Key | Type | Default | What it does |
|---|---|---|---|
rule | expression | required | What must be true. A rule that is false produces its message. |
message | locale map or string | What the editor reads, in the locale being saved (else the source locale’s). | |
fields | list of field names | the fields the rule reads | Where the message appears. Each must be a field of the type. |
severity | error or warning | error | An error stops the save (or the publish, with on: publish); a warning is advice. |
on | save or publish | every save and publish | publish runs the rule only when publishing, so a draft can be incomplete. |
Rules read the document’s top-level fields. A type with variants runs a rule only in the variants that have every field it names. The code of a rule’s problem is cross_field. Rules may be spread across the files of a type split over several files.
What a problem looks like
Through the content API and the content MCP, each problem is reported with:
| Property | Example |
|---|---|
path | stages[2].title, address.postal_code, cta.fields.icon |
code | required, max_length, cross_field, money_currency_fixed |
severity | error or warning |
message | Localised text, such as Must be 280 characters or fewer |
Each field type’s codes are on its page under Fields.
Checking without saving
engine-dmo content create --type walk --data '{...}' validates as the admin’s save does and refuses on an error; add --publish to run the publish checks too. Both need a database. The content MCP can report a document’s publish checks without publishing (Content MCP).
Form
The form field: lets an editor place one of the site's declared form endpoints on a page, with the endpoints it may offer, the checks, and the form value templates render.
Relations
The relation field: references from a document to other documents, tags, smart lists, assets, organisations or people, with targets, cardinality, tag filters, publish checks, stored shape, and how templates receive resolved entries.