viewDefinition
The UI tree a spec can carry: views, their layout, and the components inside them — plus the evaluated form a renderer receives back. The per-type field lists live in the component catalog; for the engine and the React renderer see the view system reference.
viewDefinition
The viewDefinition is a declarative JSON artifact embedded in ModelSpec that describes
component layout, field bindings, visibility rules, and event handlers. It is stored as a
raw JsonNode in ModelSpec and parsed by the valem-view module when a view is
evaluated.
The definition is renderer-agnostic. The built-in renderer (valem-view-react) is
one implementation; any client that can parse EvaluatedView JSON from the REST or console
endpoint can implement its own renderer.
Top-level ViewDefinition
"viewDefinition": {
"renderer": "builtin",
"defaultView": "main",
"views": [ ... ]
}
| Field | Required | Default | Description |
|---|---|---|---|
renderer |
no | "builtin" |
Reserved for future renderer selection; always "builtin" |
defaultView |
yes | — | id of the view shown when no viewId is specified |
views |
yes | — | Array of ViewSpec objects |
ViewSpec
{
"id": "main",
"label": "Customer Survey",
"layout": "vertical",
"columns": null,
"components": [ ... ],
"onOpen": null,
"onClose": null
}
| Field | Required | Default | Description |
|---|---|---|---|
id |
yes | — | Unique view identifier within this model |
label |
no | — | Display title for the view |
layout |
no | "vertical" |
Layout mode: "vertical", "horizontal", "grid", "tabs", "wizard" |
columns |
no | — | Column count for "grid" layout |
components |
no | [] |
Array of ComponentSpec objects |
onOpen |
no | — | EventHandler fired when the view becomes active |
onClose |
no | — | EventHandler fired when the view is navigated away from |
ComponentSpec
A flat discriminated-union record. The type field identifies the component kind; all
other fields are nullable and interpreted only for the component types that use them.
Required fields
| Field | Description |
|---|---|
id |
Unique component identifier within the view |
type |
Component type discriminator (see catalog below) |
Common fields (all component types)
| Field | Type | Description |
|---|---|---|
label |
string | Display label |
visible |
bool | JSONata string | null | Visibility; null = inherit from relevant meta |
enabled |
bool | JSONata string | null | Interactivity; null = !readOnly |
readOnly |
bool | JSONata string | null | Non-editable; null = inherit from readOnly meta |
required |
bool | JSONata string | null | Mandatory; null = inherit from required meta |
bind |
$.path string |
Model field path the component reads from / writes to |
placeholder |
string | Input placeholder text |
helperText |
string | Helper text shown below the component |
tooltip |
string | Tooltip on hover |
onChange |
EventHandler |
Fired when the bound value changes |
onOpen |
EventHandler |
Fired when a sub-panel opens |
onClose |
EventHandler |
Fired when a sub-panel closes |
Field value kinds
Every component field is one of three kinds, and putting the wrong kind of value in a field is the most common way a generated view breaks. Know which kind a field is before you fill it in:
| Kind | Fields | What to write |
|---|---|---|
| Plain text | label, placeholder, helperText, tooltip, legend, alt, addLabel/removeLabel, fromLabel/toLabel, and every options[].label / menuItems[].label / tableColumns[].header / keyValueList items[].label |
The literal string, shown verbatim. Never wrap it in quotes; never write JSONata here — it is not evaluated. "label": "Weight (kg)", not "label": "\"Weight (kg)\"" (which shows the quote characters). |
Path (bind) |
bind, bindFrom, bindTo, dependsOn, chartX, chartSeries[].field, tableColumns[].field, keyValueList items[].bind |
A $.path address the component reads its value from. This is the primary way to show a stored or derived value: "bind": "$.bmi". |
| Expression (JSONata) | text, value, delta, caption, trend, and the boolean dynamics visible / enabled / readOnly / required |
A JSONata expression evaluated against the merged document — subject to the $ rule below for the value fields. |
Dynamic expression fields and the $ rule
The boolean dynamics and the display-value expression fields are evaluated differently, and the difference is easy to trip over:
visible,enabled,readOnly,requiredaccept a JSON boolean, a JSONata string (always evaluated, no$required —"visible": "age < 20"works), ornull/absent (falls back to meta-cache inheritance, see below).-
text,value,delta,caption,trendaccept a JSONata string or a literal, but the server-sideViewEvaluatoronly evaluates the string as JSONata when it contains a$. A string with no$is shown verbatim:"text": "bmiCategory" // no $ → shows the literal word "bmiCategory" "text": "$string(bmiCategory)" // has $ → shows the value of the bmiCategory field "caption": "kg/m2" // no $ → shows the literal text "kg/m2" (correct for fixed text)To reference a model field from one of these fields, either give the component a
bind(simplest, and the value can then beformatted), or wrap the field in a$function ($string(field),$round(field, 1)). To show fixed literal text, write it plainly with no surrounding quotes and no$— it falls through to literal. Add\"...\"quotes only for a literal segment spliced into a real$-expression:"text": "$string(bmi) & \" kg/m2\"".
Server vs. built-in UI. The
$rule is a server-side (GET /models/{id}/view, MCP, console) behaviour. The bundled React renderer evaluates the raw spec client-side and will resolve a baretext: "bmiCategory"too, so a spec written without$appears to work in the built-in UI while rendering literally everywhere else. Writebindor a$-bearing expression so the view renders identically through every renderer. See view-system.md.
Rule of thumb: to show one stored or derived value, prefer "bind": "$.path". Reserve the
text/value expression fields for values you compute inline, and always include a $.
className is dead — no evaluator or renderer reads it. It is no longer modelled on the
ComponentSpec hierarchy in Java or in the TypeScript mirror. A className in an existing spec
is still stored and served verbatim, but nothing acts on it. Don’t rely on it.
Not every common field applies to every type — each component type binds to the ComponentSpec
record carrying the fields it uses, so e.g. a separatorLine has no placeholder and a badge
has no enabled. See the record table in
view-system.md for the exact per-type field sets.
Meta cache inheritance (null defaults)
When a dynamic field is absent or null, the view evaluator looks up the meta cache:
| Field | Meta cache key | Absent default |
|---|---|---|
visible |
$.bind#relevant — false means hidden; missing means visible |
true |
readOnly |
$.bind#read_only — true means read-only; missing means editable |
false |
required |
$.bind#required |
false |
enabled |
— | !effectiveReadOnly |
This lets metaDerivations drive component visibility and interactivity with zero
extra view configuration. For example, if issueCategory has a relevant metaDerivation
that evaluates to false (because issueEncountered = false), the component is
automatically hidden — no visible expression needed in the view definition.
EventHandler
Defines what happens when a UI event fires. Used by onClick, onChange, onOpen,
onClose on ComponentSpec, and onOpen, onClose on ViewSpec.
{
"mutations": "{'$.status': 'submitted', '$.submittedAt': $now()}",
"navigate": "confirmation"
}
| Field | Description |
|---|---|
mutations |
JSONata expression evaluated against the model state; result must be a {"$.path": value} map |
navigate |
View id to activate after mutations are applied |
Both fields are optional (either, neither, or both may be present).
EvaluatedView — REST / Console response
GET /models/{id}/view and GET /models/{id}/view/{viewId} return an EvaluatedView
with all dynamic expressions already resolved. This is the renderer-agnostic contract; any
client (React, Angular, mobile, CLI) can consume it.
EvaluatedComponent is a sealed interface, not a flat record — each component type
serializes as one of 26 concrete records (EvaluatedBasicInput, EvaluatedTextArea,
EvaluatedSelectField, EvaluatedDependentSelector, EvaluatedSlider, EvaluatedDateRange,
EvaluatedFileUpload, EvaluatedLabel, EvaluatedStaticText, EvaluatedBadge,
EvaluatedImage, EvaluatedLink, EvaluatedProgressBar, EvaluatedDataTable,
EvaluatedDataChart, EvaluatedKeyValueList, EvaluatedStatTile, EvaluatedJsonViewer,
EvaluatedTracePanel, EvaluatedValidationSummary, EvaluatedEffectStatus,
EvaluatedContainer, EvaluatedSectionList, EvaluatedButton, EvaluatedMenu,
EvaluatedSeparatorLine) carrying only the fields relevant to that type
(@JsonInclude(NON_NULL)); the JSON below shows the union of possible fields, not a shape any
single component actually emits in full.
Three of them carry a declaration rather than data: explainPanel, auditTimeline and
validationSummary evaluate to which path to explain and how many rows to ask for, because the
evaluator receives only the merged document, meta cache, expression cache and constants — it has
no access to the trace ring buffer, the audit store or the flagged-constraint set. The renderer
fetches those itself. See view-system.md.
{
"modelId": "customer-satisfaction-survey",
"viewId": "main",
"title": "Survey",
"layout": "vertical",
"components": [
{
"id": "ratingField",
"type": "radioField",
"label": "Overall Satisfaction",
"visible": true,
"enabled": true,
"readOnly": false,
"required": true,
"bind": "$.overallRating",
"value": 5,
"options": [
{ "value": "1", "label": "1 — Very dissatisfied" },
{ "value": "5", "label": "5 — Very satisfied" }
],
"placeholder": null,
"helperText": null,
"tooltip": null,
"text": null,
"components": null,
...
}
]
}
EvaluatedComponent fields
| Field | Type | Description |
|---|---|---|
id |
string | Component identifier |
type |
string | Component type |
label |
string | Resolved display label |
visible |
boolean | Whether the component should be shown |
enabled |
boolean | Whether the component is interactive |
readOnly |
boolean | Whether the component is non-editable |
required |
boolean | Whether the field is mandatory |
bind |
string | Bound model path |
value |
JSON value | Current value from the merged model document |
placeholder |
string | Placeholder text |
helperText |
string | Helper text |
tooltip |
string | Tooltip |
options |
[{value, label}] |
Resolved options list |
text |
string | Resolved text (for label, staticText, badge, alert, link) |
components |
EvaluatedComponent[] |
Resolved sub-components (for aggregates) |
collapsed |
boolean | Initial fold state (for collapsible, accordion, jsonViewer, the trace panels) |
tableColumns |
ColumnSpec[] |
Column definitions (for dataTable) |
pageSize |
integer | Rows per page (for dataTable) |
chartType |
string | Chart type (for dataChart, sparkline) |
chartX |
string | X-axis field (for dataChart) |
chartSeries |
ChartSeriesSpec[] |
Series definitions (for dataChart, sparkline) |
items |
EvaluatedKeyValueItem[] |
Resolved caption/value rows (for keyValueList) — each {label, bind, value, text, format, currency} |
delta / caption / trend |
string | Resolved supporting text (for statTile) |
bindFrom / bindTo |
string | The two bound paths (for dateRangeField) |
valueFrom / valueTo |
JSON value | The two resolved ends (for dateRangeField) |
src / alt / fit |
string | Image source, alternative text, object-fit (for image) |
href / target |
string | Anchor destination (for link) |
limit / showConstraints |
integer / boolean | How many rows to fetch (for explainPanel, auditTimeline) — a declaration; the rows are not resolved server-side |
pathPrefix / maxItems / emptyText |
string / integer / string | Scope and empty state (for validationSummary) |
effectId / errorPath / error / showRetry / retryLabel |
Effect state (for effectStatus); error is resolved, being ordinary model state |
|
maxDepth |
integer | Truncation depth (for jsonViewer) |
menuItems |
MenuItemSpec[] |
Navigation items (for menu, stepper, breadcrumb) |
orientation |
string | Layout orientation (for menu, stepper) |
variant |
string | Visual variant (for button, badge, alert, statTile, validationSummary); unevaluated raw string for badge — see the badge known-gap note above |
icon |
string | Icon name |
min / max |
number | Range bounds (for sliderField, ratingField, numericStepper, progressBar, gauge) |
step |
number | Step increment (for sliderField, ratingField, numericStepper) |
allowCustom |
boolean | Accept a value outside options (for the choice types) |
toolbar |
string | Editing chrome (for richTextField) |
size |
integer | Gap in pixels (for spacer) |
accept |
string | MIME filter (for fileUploadField) |
multiple |
boolean | Allow multiple files (for fileUploadField) |
minFiles / maxFiles |
integer | File-count bounds (for fileUploadField) |
minSize / maxSize |
number | Per-file byte-size bounds (for fileUploadField) |
allowedMediaTypes |
string | Allowed media types (for fileUploadField) |
showValue |
boolean | Display numeric label alongside the bar (for progressBar, gauge) |
format |
string | "percent" or "value" (for progressBar); "currency" / "percent" / "number" / "integer" for the numeric inputs and statTile, filled in from the type when unset |
currency |
string | ISO-4217 code (for currencyField, statTile, and per keyValueList row) |
onClick |
EventHandler |
Click handler (passed through unevaluated for client execution) |
onChange |
EventHandler |
Change handler |
onOpen |
EventHandler |
Open handler |
onClose |
EventHandler |
Close handler |
Note: EventHandler objects (onClick, onChange, etc.) in EvaluatedComponent are
passed through unevaluated — they contain the raw mutations JSONata string and navigate
id. The client renderer executes them locally when the event fires.