Schema reference
This is the contract between your backend and the engine. Anything here can change without a frontend deploy.
The root
Section titled “The root”{ "id": "quote-form", "fields": [ ]}| Property | Type | Required | Notes |
|---|---|---|---|
fields | FieldSchema[] | yes | The node tree. Containers nest arbitrarily; leaves are inputs. |
id | string | number | no | Passed back to onFormLoad. Useful for multi-step flows. |
| anything else | unknown | no | Passed through untouched — a title, a version, a note. |
Fields
Section titled “Fields”Every node is a FieldSchema. A node with a fields array is a
container — it groups and lays out children but holds no value. A node
without one is a leaf: a real input that stores a value.
Form data stays flat regardless of nesting, so every leaf name must be
unique across the whole schema. The parser warns on duplicates.
Identity and ordering
Section titled “Identity and ordering”| Property | Type | Required | Notes |
|---|---|---|---|
name | string | yes | The key in form data. Unique across the whole schema. |
type | string | yes | Looked up in your field registry. Unregistered types warn and are skipped rather than breaking the page. |
order | number | yes | Ascending sort within the node’s siblings. Re-order a form by changing numbers. |
label | string | no | Your component decides how to render it. |
value | unknown | no | Initial value. For a repeater, an array of row objects. |
columnGrid | number | no | Layout hint passed to your component (e.g. 1–12). The engine doesn’t interpret it. |
Conditional behaviour
Section titled “Conditional behaviour”Each takes a boolean or an expression string. Expressions are re-evaluated whenever a referenced field changes.
| Property | Type | Notes |
|---|---|---|
show | string | Hidden fields unmount, and their validation rules unregister with them. On a container, the whole subtree goes. |
required | boolean | string | Drives isRequired from useBlockField. Pair with a required validator to actually block submit. |
disabled | boolean | string | Drives isDisabled. |
calculateValue | string | Derives the value from other fields. See calculated values. |
Validation
Section titled “Validation”| Property | Type | Notes |
|---|---|---|
validation | ValidationRule[] | Applied in order. |
A ValidationRule:
| Property | Type | Notes |
|---|---|---|
name | string | A built-in, or anything you registered on the validator registry. |
value | unknown | Argument for parameterised rules (minLength: 3). |
message | string | Shown when the rule fails. |
validateOn | 'change' | 'blur' | 'submit' | Defaults to 'change'. |
Built-in rules: required, minLength, maxLength, min, max,
length, range, email, url, pattern.
When each mode runs: on change, only change rules. On blur, change and
blur rules. On submit, all rules regardless of setting. Use blur for
async server checks so they don’t fire on every keystroke.
Custom and async rules
Section titled “Custom and async rules”A validator is a function returning an error message or null, and it may
return a Promise. It receives the whole form data, so cross-field rules need no
wiring in your components:
const validators = createValidatorRegistry()
validators.register('unique', async (value) => { const taken = await api.checkUsername(String(value)) return taken ? 'Already taken' : null})
validators.register('matches', (otherField) => (value, formData) => value === formData?.[String(otherField)] ? null : `Doesn't match ${otherField}`,)Pass the registry to useDynamicField or <DrivenForm validatorRegistry={…} />,
then reference the rules by name from any schema.
Options
Section titled “Options”| Property | Type | Notes |
|---|---|---|
options | FieldOption[] | For selects, radios, multi-selects. |
A FieldOption is { id: string | number, label: string } plus any extra keys
your component needs — passed through untouched.
Presentation
Section titled “Presentation”| Property | Type | Notes |
|---|---|---|
helpText | string | Helper copy below the input. |
placeholder | string | Placeholder text. |
isHidden | boolean | Value stays in form data but the field never renders. For values expressions need but users shouldn’t see. |
isHidden and show are different: show: false unmounts the field and
unregisters its validation; isHidden keeps it fully live and merely invisible.
Containers
Section titled “Containers”| Property | Type | Notes |
|---|---|---|
fields | FieldSchema[] | Presence makes this node a container. |
Register container types with registry.registerContainer(type, Component).
A container with no registered component renders its children bare — handy for
grouping fields under one show expression without adding markup.
Repeaters
Section titled “Repeaters”| Property | Type | Notes |
|---|---|---|
columnFields | FieldSchema[] | The row template. Full field schemas — validation, expressions and nesting all work per row. |
minRows | number | Remove is disabled at this count. Default 0. |
maxRows | number | Add is disabled at this count. Default unlimited. |
initialRows | number | Rows to seed when value is absent. Falls back to minRows. |
Seeding order is value → initialRows → minRows → none. The value is an
array of row objects, and that’s exactly what onSave receives. See
repeaters & tables.
Custom properties
Section titled “Custom properties”FieldSchema has an index signature: any key you add is passed through to
your component untouched as field.yourKey. Icons, column widths, analytics
ids, a currency symbol — the engine neither validates nor interprets them.
That’s the escape hatch that keeps the schema from needing to grow a property every time a design does.
A complete example
Section titled “A complete example”{ "id": "onboarding", "fields": [ { "name": "accountType", "type": "radio", "label": "Account type", "order": 1, "options": [ { "id": "individual", "label": "Individual" }, { "id": "business", "label": "Business" } ], "validation": [{ "name": "required", "message": "Pick one" }] }, { "name": "businessDetails", "type": "section", "label": "Business details", "order": 2, "show": "%accountType% == 'business'", "fields": [ { "name": "companyName", "type": "text", "label": "Registered name", "order": 1, "required": true, "validation": [{ "name": "required", "message": "Required" }] }, { "name": "employees", "type": "number", "label": "Employees", "order": 2, "helpText": "Approximate is fine.", "validation": [{ "name": "min", "value": 1, "message": "Must be at least 1" }] } ] } ]}