@mackehansson/schemaform

The Three Layers

How JSON Schema, UI Schema, and Rules fit into a versioned Schemaform Form Definition.

A Schemaform Form Definition bundles three documents under one format version:

{
  formatVersion: 1,
  schema: {},   // JSON Schema: data and validation
  uiSchema: {}, // UI Schema: Pages, layout, and Controls
  rules: [],    // Rules: behavior evaluated against Working State
}

Each layer answers a different question. Keeping those answers separate makes the Form Builder easier to reason about, and it lets the Form Renderer interpret the same definition without guessing intent.

JSON Schema: data and validation

JSON Schema defines the shape of the submitted data and the constraints that make data valid. Field types, required fields, format, enum, minLength, and array limits live here.

If a choice changes whether data is valid, it belongs in JSON Schema.

In the example below, name has minLength: 2, email has format: "email", and applicantType is limited to two values. Those are data rules, not presentation details.

UI Schema: presentation

UI Schema is the presentation layer. It is a layout tree of Pages, layout nodes, and Controls. A Page is one top-level step or tab in a form. A Control renders a single JSON Schema property by pointing at it with a JSON Pointer scope such as #/properties/email.

If a choice changes how the form looks, it belongs in UI Schema.

For example, notes is still a JSON Schema string, but its Control uses widget: "textarea" so the Renderer chooses a textarea from its Registry. That does not change what data is valid; it changes how the field is rendered.

Rules: behavior

Rules describe behavior evaluated against the current Working State before validation runs. A Rule is a persisted document with a JSONLogic condition, an effect, and a target Control, group, or Page.

If a choice changes visibility, enabled state, or required state based on current form data, it belongs in Rules.

In the example, companyName is shown and required only when applicantType is "Company". The JSON Schema still describes companyName as a string with a minimum length; the Rules decide when the Control matters in the current interaction.

Live form

Application

Applicant type

Source

import type { FormDefinition } from "@mackehansson/schemaform";import { SchemaForm } from "@mackehansson/schemaform-shadcn";const applicationForm = {  formatVersion: 1,  schema: {    type: "object",    required: ["name", "email", "applicantType"],    properties: {      name: {        type: "string",        title: "Name",        minLength: 2,      },      email: {        type: "string",        title: "Email",        format: "email",      },      applicantType: {        type: "string",        title: "Applicant type",        enum: ["Individual", "Company"],      },      companyName: {        type: "string",        title: "Company name",        minLength: 2,      },      notes: {        type: "string",        title: "Notes",      },    },  },  uiSchema: {    pages: [      {        type: "Page",        title: "Application",        children: [          { type: "Control", scope: "#/properties/name" },          { type: "Control", scope: "#/properties/email" },          {            type: "Control",            scope: "#/properties/applicantType",            widget: "radio",          },          {            type: "Control",            scope: "#/properties/companyName",          },          {            type: "Control",            scope: "#/properties/notes",            widget: "textarea",          },        ],      },    ],  },  rules: [    {      condition: { "==": [{ var: "applicantType" }, "Company"] },      effect: "show",      target: "#/properties/companyName",    },    {      condition: { "==": [{ var: "applicantType" }, "Company"] },      effect: "require",      target: "#/properties/companyName",    },  ],} satisfies FormDefinition;export function App() {  return (    <SchemaForm      definition={applicationForm}      onSubmit={(data) => console.log(data)}    />  );}

Where does minLength go?

Dev: "Where do I put minLength — the JSON Schema or the UI Schema?"

Domain expert: "JSON Schema. It changes what data is valid. The UI Schema only changes how the field looks, such as rendering that string as a textarea instead of an input."

That rule of thumb works for most modeling decisions:

QuestionLayer
What shape is the submitted data?JSON Schema
Is this field required for valid data?JSON Schema
Which Page contains this field?UI Schema
Should this string render as a textarea?UI Schema
Should this Control hide when another value changes?Rules
Should a field become required only in one branch?Rules

The Form Builder edits all three layers. The Form Renderer consumes the resulting Form Definition, evaluates Rules against Working State, renders the visible Controls, and produces the Submission Payload when the user submits.

On this page