Skip to content

Recipes

Common patterns and integration scenarios for the JSON Form Builder.

Importing Existing Schemas

If you have existing JSON Schema and UI Schema from another source, you can load them into the builder for editing:

ts
import { useBuilderStore } from '@banclo/jsonforms-core'

const store = useBuilderStore()

const jsonSchema = {
  type: 'object',
  properties: {
    firstName: { type: 'string', minLength: 1 },
    lastName: { type: 'string' },
    age: { type: 'integer', minimum: 0, maximum: 150 },
  },
  required: ['firstName'],
}

const uiSchema = {
  type: 'VerticalLayout',
  elements: [
    {
      type: 'HorizontalLayout',
      elements: [
        { type: 'Control', scope: '#/properties/firstName' },
        { type: 'Control', scope: '#/properties/lastName' },
      ],
    },
    { type: 'Control', scope: '#/properties/age' },
  ],
}

store.importSchema(jsonSchema, uiSchema)

The importSchema action parses the UI Schema recursively, creates a SchemaNode tree, and looks up each control's schema properties from the JSON Schema. This operation pushes the current state to the undo stack, so the user can undo the import.

Exporting Schemas

Retrieve the generated schemas for saving or sending to a server:

ts
import { useBuilderStore } from '@banclo/jsonforms-core'

const store = useBuilderStore()

// Via the convenience method
const { jsonSchema, uiSchema } = store.exportSchema()

// Or read the computed refs directly
console.log(store.jsonSchema)
console.log(store.uiSchema)

// Save to your backend
await fetch('/api/forms', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ jsonSchema, uiSchema }),
})

Programmatic Form Building

You can build forms entirely in code without the visual builder:

ts
import {
  createSchemaNode,
  addElement,
  generateJsonSchema,
  generateUiSchema,
} from '@banclo/jsonforms-core'

// Create a root layout
const root = createSchemaNode('VerticalLayout')

// Create controls
const firstName = createSchemaNode('Control', {
  propertyName: 'firstName',
  schemaProperties: { type: 'string', minLength: 1 },
  label: 'First Name',
})

const email = createSchemaNode('Control', {
  propertyName: 'email',
  schemaProperties: { type: 'string', format: 'email' },
  label: 'Email Address',
})

const subscribe = createSchemaNode('Control', {
  propertyName: 'subscribe',
  schemaProperties: { type: 'boolean' },
  label: 'Subscribe to newsletter',
})

// Build the tree
let tree = addElement(root, firstName, root.id)
tree = addElement(tree, email, root.id)
tree = addElement(tree, subscribe, root.id)

// Generate output
const jsonSchema = generateJsonSchema(tree)
const uiSchema = generateUiSchema(tree)

Integrating with a Form Rendering App

A common architecture is to have a "builder" app for form authors and a "renderer" app for end users. The builder produces JSON Schema + UI Schema; the renderer displays the form.

Builder App

vue
<script setup lang="ts">
import { FormBuilder } from '@banclo/jsonforms-ui'
import { useBuilderStore } from '@banclo/jsonforms-core'

const store = useBuilderStore()

async function saveForm() {
  const { jsonSchema, uiSchema } = store.exportSchema()
  await fetch('/api/forms/my-form', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonSchema, uiSchema }),
  })
}
</script>

<template>
  <div class="h-screen flex flex-col">
    <header class="p-4 border-b flex justify-between items-center">
      <h1>Form Builder</h1>
      <button @click="saveForm">Save</button>
    </header>
    <FormBuilder class="flex-1" />
  </div>
</template>

Here's what that builder integration looks like in action:

Renderer App

vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { JsonForms } from '@jsonforms/vue'
import { vanillaRenderers } from '@jsonforms/vue-vanilla'

const jsonSchema = ref({})
const uiSchema = ref({})
const formData = ref({})

onMounted(async () => {
  const res = await fetch('/api/forms/my-form')
  const form = await res.json()
  jsonSchema.value = form.jsonSchema
  uiSchema.value = form.uiSchema
})

function handleChange(event: { data: unknown }) {
  formData.value = event.data as Record<string, unknown>
}
</script>

<template>
  <JsonForms
    :schema="jsonSchema"
    :uischema="uiSchema"
    :data="formData"
    :renderers="vanillaRenderers"
    @change="handleChange"
  />
</template>

Custom Validation

Validate the builder's tree before exporting to catch structural problems:

ts
// NOTE: `validateSchemaNode` and `validateGeneratedSchemas` may need to be imported from
// '@banclo/jsonforms-core/codegen/validator' if not re-exported from the main package entry.
import { useBuilderStore, validateSchemaNode, validateGeneratedSchemas } from '@banclo/jsonforms-core'

const store = useBuilderStore()

// Validate the node tree
const treeResult = validateSchemaNode(store.rootNode)
if (!treeResult.valid) {
  console.error('Tree validation errors:', treeResult.errors)
}
if (treeResult.warnings.length > 0) {
  console.warn('Tree validation warnings:', treeResult.warnings)
}

// Validate the generated schemas
const { jsonSchema, uiSchema } = store.exportSchema()
const schemaResult = validateGeneratedSchemas(jsonSchema, uiSchema)
if (!schemaResult.valid) {
  console.error('Schema validation errors:', schemaResult.errors)
}

ValidationResult

Both validation functions return the same shape:

ts
interface ValidationResult {
  valid: boolean
  errors: ValidationError[]
  warnings: ValidationWarning[]
}

interface ValidationError {
  path: string       // Location in the tree or schema
  message: string    // Human-readable description
  nodeId?: string    // The SchemaNode ID, if applicable
}

What Gets Validated

Tree validation (validateSchemaNode):

  • Controls without a propertyName
  • Duplicate property names across controls
  • Category nodes placed outside Categorization
  • Non-Category children inside Categorization
  • Rule conditions referencing unknown scopes

Schema validation (validateGeneratedSchemas):

  • JSON Schema root type is "object"
  • properties key exists and is an object
  • required array entries match actual properties
  • UI Schema has a type
  • Control scopes reference existing JSON Schema properties

Conditional Fields with Rules

Rules let you show, hide, enable, or disable elements based on the value of other fields. You can set rules through the property panel or programmatically:

ts
import { useBuilderStore } from '@banclo/jsonforms-core'

const store = useBuilderStore()

// After adding nodes, set a rule on a control
store.updateNode(emailNodeId, {
  rule: {
    effect: 'SHOW',
    condition: {
      type: 'LEAF',
      scope: '#/properties/subscribe',
      expectedValue: true,
    },
  },
})

This rule makes the email field visible only when the subscribe checkbox is checked.

Composite Conditions

You can combine multiple conditions with AND/OR:

ts
store.updateNode(discountCodeId, {
  rule: {
    effect: 'ENABLE',
    condition: {
      type: 'AND',
      conditions: [
        {
          type: 'LEAF',
          scope: '#/properties/memberType',
          expectedValue: 'premium',
        },
        {
          type: 'LEAF',
          scope: '#/properties/acceptTerms',
          expectedValue: true,
        },
      ],
    },
  },
})

Try it -- the form below has a SHOW rule on "Email" (visible when "Subscribe" is checked) and an ENABLE rule on "Discount Code" (enabled when member type is "premium" AND terms are accepted). Click on the fields to inspect their rules in the property panel:

Schema-Based Conditions

For more complex logic, use schema-based conditions that check values against a JSON Schema:

ts
store.updateNode(warningLabelId, {
  rule: {
    effect: 'SHOW',
    condition: {
      type: 'SCHEMA',
      scope: '#/properties/age',
      schema: { minimum: 65 },
    },
  },
})

This shows the warning label only when the age field has a value of 65 or higher.

Resetting the Builder

To clear all content and start with a fresh canvas:

ts
const store = useBuilderStore()
store.reset()

This pushes the current state to the undo stack, creates a new empty root VerticalLayout, clears selection and drag state, and resets form metadata.

Working with the Node Map

The builder store maintains a nodeMap for O(1) lookups by node ID:

ts
const store = useBuilderStore()

// Look up any node by ID
const node = store.nodeMap.get(someNodeId)

// Check if a node exists
if (store.nodeMap.has(someNodeId)) {
  // ...
}

// Iterate all nodes
for (const [id, node] of store.nodeMap) {
  if (node.type === 'Control') {
    console.log(node.propertyName)
  }
}