Skip to main content

Advanced UI schema

This page covers dynamic behavior in UI schema: rules for conditional visibility, enabling/disabling and the APPLY effect to patch labels and options at runtime. You’ll also find recipes, troubleshooting, and performance tips.

Rules at a glance

Rules let you react to form data. A rule is attached to any UI schema element via the rule property.

Supported effects

  • SHOW — show when condition is true (hide otherwise)
  • HIDE — hide when condition is true (show otherwise)
  • ENABLE — enable when condition is true (disable otherwise)
  • DISABLE — disable when condition is true (enable otherwise)
  • APPLYpatch the UI schema element when condition is true (or via multi-case)
{
"type": "Control",
"scope": "#/properties/ssn",
"rule": {
"effect": "SHOW",
"condition": {
"scope": "#/properties/country",
"schema": { "const": "USA" }
}
}
}

Conditions

Conditions define when a rule takes effect. You’ll mostly use schema conditions, but complex flows can combine them.

Schema condition (most common)

Validates a field value against a JSON schema snippet.

{
"scope": "#/properties/country",
"schema": { "const": "USA" }
}
  • Use const for equality checks (strings, booleans, numbers)
  • Use enum, minimum, pattern, etc., for richer checks

Logical conditions (AND / OR)

Combine conditions to express more advanced logic.

{
"type": "AND",
"conditions": [
{ "scope": "#/properties/isVIP", "schema": { "const": true } },
{ "scope": "#/properties/orders", "schema": { "minimum": 10 } }
]
}
{
"type": "OR",
"conditions": [
{ "scope": "#/properties/tier", "schema": { "const": "gold" } },
{ "scope": "#/properties/tier", "schema": { "const": "platinum" } }
]
}

Leaf equality

A shorthand to match the current field value exactly (used inside APPLY cases).

{ "type": "LEAF", "expectedValue": "advanced" }

Function condition (expert)

When needed, provide a custom validator. This gives you full access to data, fullData, path, and the current uischemaElement.

{
"validate": "(ctx) => Array.isArray(ctx.fullData.products) && ctx.fullData.products.length > 0"
}
Use sparingly

Custom functions are powerful but harder to test and review. Prefer schema and logical conditions first.


APPLY: dynamic patches

APPLY lets you patch UI schema properties (like label, options, or any element property) based on form state.

Single outcome

{
"type": "Control",
"scope": "#/properties/quantity",
"rule": {
"effect": "APPLY",
"condition": { "scope": "#/properties/inStock", "schema": { "const": false } },
"outcome": { "enabled": false, "label": "Quantity (Out of stock)" }
}
}

Multi-case outcomes

Evaluate cases in order; the first match wins. Provide a final catch-all outcome without condition.

{
"type": "Control",
"scope": "#/properties/field",
"rule": {
"effect": "APPLY",
"cases": [
{
"condition": { "scope": "#/properties/mode", "schema": { "const": "basic" } },
"outcome": { "label": "Basic field", "options": { "multi": false } }
},
{
"condition": { "type": "LEAF", "expectedValue": "advanced" },
"outcome": { "label": "Advanced field", "options": { "multi": true } }
},
{
"outcome": { "label": "Default field" }
}
]
}
}

Dynamic options (unset/override)

{
"type": "Control",
"scope": "#/properties/password",
"options": { "format": "password" },
"rule": {
"effect": "APPLY",
"condition": { "scope": "#/properties/showPassword", "schema": { "const": true } },
"outcome": { "options": { "format": null } }
}
}
Pattern

Use outcome.options to tweak how a control renders without duplicating the element.


Common dynamic patterns

Show a section when a toggle is ON

{
"type": "Group",
"label": "Delivery settings",
"rule": { "effect": "SHOW", "condition": { "scope": "#/properties/delivery", "schema": { "const": true } } },
"elements": [
{ "type": "Control", "scope": "#/properties/address" },
{ "type": "Control", "scope": "#/properties/window" }
]
}

Enable a field only for admins

{
"type": "Control",
"scope": "#/properties/limit",
"rule": { "effect": "ENABLE", "condition": { "scope": "#/properties/isAdmin", "schema": { "const": true } } }
}

Change label based on selection

{
"type": "Control",
"scope": "#/properties/name",
"rule": {
"effect": "APPLY",
"cases": [
{ "condition": { "scope": "#/properties/type", "schema": { "const": "person" } }, "outcome": { "label": "Full name" } },
{ "condition": { "scope": "#/properties/type", "schema": { "const": "company" } }, "outcome": { "label": "Company name" } },
{ "outcome": { "label": "Name" } }
]
}
}

Derive options based on other data

{
"type": "Control",
"scope": "#/properties/region",
"rule": {
"effect": "APPLY",
"condition": { "scope": "#/properties/country", "schema": { "enum": ["US", "CA"] } },
"outcome": { "options": { "placeholder": "Pick a region (US/CA)" } }
}
}

Stepper with nav buttons

{
"type": "Categorization",
"options": { "variant": "stepper", "showNavButtons": true },
"elements": [
{ "type": "Category", "label": "General", "elements": [ { "type": "Control", "scope": "#/properties/title" } ] },
{ "type": "Category", "label": "Details", "elements": [ { "type": "Control", "scope": "#/properties/description" } ] }
]
}

Arrays: show sort buttons only when > 1 item

{
"type": "Control",
"scope": "#/properties/tasks",
"options": { "showArrayLayoutSortButtons": false },
"rule": {
"effect": "APPLY",
"condition": { "validate": "(ctx) => Array.isArray(ctx.fullData.tasks) && ctx.fullData.tasks.length > 1" },
"outcome": { "options": { "showArrayLayoutSortButtons": true } }
}
}

Troubleshooting

My rule doesn’t trigger

  • ✅ Check the scope pointer — it must match the JSON Schema path (e.g., #/properties/featureFlag)
  • ✅ Validate the condition schema — prefer const for equality checks
  • ✅ Ensure the data type matches (e.g., "true" vs true)
  • ✅ If using arrays/objects, remember item contexts (e.g., #/items/properties/name inside array detail)

The control is hidden even when it should show

  • If using SHOW, it hides when the condition is false. Switch to HIDE if you want the inverse behavior.
  • Check for multiple rules affecting the same element (e.g., parent Group hidden).

APPLY didn’t change my option

  • Confirm the outcome shape: { "outcome": { "options": { ... } } }
  • To unset an option, set it to null (e.g., "format": null).
  • Remember that first matching case wins — ensure case order is correct.

Reference: rule schemas (for copy/paste)

// Rule with a single condition
{
effect: "SHOW" | "HIDE" | "ENABLE" | "DISABLE" | "APPLY",
condition: SchemaCondition | LogicalCondition | LeafCondition | FunctionCondition,
outcome?: ApplyOutcome
}

// Multi-case APPLY
{
effect: "APPLY",
cases: Array<{
condition?: SchemaCondition | LogicalCondition | LeafCondition | FunctionCondition,
outcome: ApplyOutcome
}>
}

// Condition shapes
type SchemaCondition = { scope: string; schema: object; failWhenUndefined?: boolean };
type LogicalCondition = { type: "AND" | "OR"; conditions: Array<Condition> };
type LeafCondition = { type: "LEAF"; expectedValue: any };
type FunctionCondition = { validate: string /* JS source as string */ };

// Outcome patches any UI schema element fields
type ApplyOutcome = { [key: string]: any };