Skip to main content

Core Concepts

JSON Schema as the Single Source of Truth​

The form structure, field types, validation rules, and conditional logic are all encoded in a single JSON Schema object. The library parses this schema at mount time and constructs an internal node tree — one node per schema field. You never manually define form fields or validation rules in component code.

const schema = {
type: 'object',
properties: {
title: { type: 'string', minLength: 1 },
price: { type: 'number', minimum: 0 },
inStock: { type: 'boolean' },
tags: { type: 'array', items: { type: 'string' } },
},
required: ['title', 'price'],
};
// → ObjectNode
// ├── StringNode (title)
// ├── NumberNode (price)
// ├── BooleanNode (inStock)
// └── ArrayNode (tags)
// └── StringNode (items)

Node Tree​

Each node is a reactive object that owns its own value, validation errors, and state flags (dirty, touched, validated). Changes propagate upward: when a leaf node's value changes, all ancestor nodes receive an UpdateValue event and update their aggregated value.

Node State Flags​

PropertyTypeDescription
valueanyCurrent value
defaultValueanyValue at initialization
errorsJSONSchemaError[]Validation errors for this node
dirtybooleanValue changed since initialization
touchedbooleanUser has interacted with this field
validatedbooleanValidation has been run
visiblebooleanComputed — controls visibility
activebooleanComputed — controls active state
readOnlybooleanComputed or inherited
disabledbooleanComputed or inherited
initializedbooleanNode has completed initialization
requiredbooleanField is required in parent schema
nullablebooleanSchema type includes "null"

Node Navigation​

// Absolute path from root (RFC 6901 JSONPointer)
const node = rootNode.find('/address/street');

// Relative path from current node
const sibling = node.find('./city');

// Find all matching nodes
const allItems = rootNode.findAll('/items/*');

Plugin Architecture​

The library is split into two independent plugin layers. You can mix and match any validator with any UI library.

┌─────────────────────────────────────────────────────┐
│ SchemaFormPlugin │
│ │
│ FormGroup FormLabel FormInput FormError │ ← UI layer
│ formTypeInputDefinitions │
├─────────────────────────────────────────────────────┤
│ ValidatorPlugin │
│ │
│ bind(instance) │ ← Validation layer
│ compile(jsonSchema) → ValidateFunction │
└─────────────────────────────────────────────────────┘

Plugin Merge Behavior​

When multiple plugins are registered, the merge rules are:

PropertyMerge strategy
FormGroup, FormLabel, FormInput, FormErrorLast wins
formTypeInputDefinitionsPrepended (first match wins)
validatorLast wins
formatErrorLast wins

FormType System​

A FormTypeInputDefinition pairs a test condition with a React component. The library evaluates all registered definitions in priority order and uses the first matching component to render a field.

The test can be a plain object (declarative) or a function (imperative):

// Object test — declarative
const definition: FormTypeInputDefinition = {
test: { type: 'string', format: 'date' }, // matches date fields
Component: DatePickerInput,
};

// Function test — imperative
const definition: FormTypeInputDefinition = {
test: (hint) => hint.jsonSchema.widget === 'richtext',
Component: RichTextEditor,
};

// Hint object available in test function:
type Hint = {
type: JSONSchemaType; // 'string' | 'number' | 'boolean' | ...
path: string; // JSONPointer path
required: boolean;
nullable: boolean;
jsonSchema: JSONSchema;
format?: string;
formType?: string; // custom jsonSchema.formType field
};

FormTypeInputMap (path-based override)​

// Map specific paths to specific components
const formTypeInputMap: FormTypeInputMap = {
'/user/avatar': AvatarUploader,
'/items/*/price': CurrencyInput, // * matches any segment
};

<Form jsonSchema={schema} formTypeInputMap={formTypeInputMap} />

Computed Properties​

Fields can react to sibling or parent values using JSONPointer expressions in the computed (or &computed) key. Dependencies are tracked automatically from the expression, or declared explicitly via watch.

{
type: 'object',
properties: {
isPremium: { type: 'boolean' },
discount: {
type: 'number',
computed: {
visible: '../isPremium === true', // show only for premium
readOnly: '../isPremium === false', // lock if not premium
watch: ['../isPremium'], // explicit dep (optional)
},
},
},
}

Supported computed keys: visible, active, readOnly, disabled, watch.

JSONPointer Extensions (computed scope only)​

SyntaxMeaning
../fieldParent node's field
./fieldCurrent node's field (relative)
..Navigate to parent
*Wildcard — any segment (FormTypeInputMap only)

Schema Composition​

The library supports all standard JSON Schema composition keywords. oneOf and anyOf branches can use the custom &if expression to bind activation to a field value.

// Standard if/then/else
{
if: { properties: { type: { enum: ['movie'] } } },
then: { properties: { releaseDate: { type: 'string', format: 'date' } } },
else: { properties: { version: { type: 'string' } } },
}

// oneOf with &if expression (exclusive branches)
{
oneOf: [
{ '&if': "./type === 'movie'", properties: { director: { type: 'string' } } },
{ '&if': "./type === 'game'", properties: { platform: { type: 'string' } } },
],
}

// anyOf with &if (non-exclusive, multiple can be active)
{
anyOf: [
{ '&if': './flag1 === true', properties: { field1: { type: 'string' } } },
{ '&if': './flag2 === true', properties: { field2: { type: 'string' } } },
],
}

// allOf (always merged)
{
allOf: [
{ properties: { firstName: { type: 'string' } } },
{ properties: { lastName: { type: 'string' } } },
{ required: ['firstName', 'lastName'] },
],
}

Conditional setValue Filtering​

When a parent node calls setValue(), fields that do not match the active oneOf/anyOf branch are automatically removed from the value. When a child node calls setValue(), this filtering is bypassed — the value propagates regardless of the current branch condition.

Array Output Filters​

Array schemas accept output filters under options. They refine only what the array emits — parent propagation, root validation, and the external surfaces (onChange, getValue(), submit) — while child nodes and the raw node.value keep every item, so pre-exposed empty inputs stay rendered.

const jsonSchema = {
type: 'array',
minItems: 3, // three empty inputs render up front
items: { type: 'string' },
options: { omitTrailing: true },
};
// The user fills only the first input:
// getValue() / onChange / validation see ['first']
  • omitTrailing (opt-in) removes trailing consecutive undefined items only. Leading and middle undefined items are preserved so error dataPaths and validation indices stay aligned.
  • omitEmpty (on by default) converts an empty array to undefined on the parent-propagation path. Filter order is omitTrailing → omitEmpty.
  • The refined output is exposed as node.normalizedValue; node.value stays raw. In a custom array FormTypeInput, count rows from node.children, never value.length.

Validation System​

ValidationMode​

enum ValidationMode {
None = 0, // No automatic validation
OnChange = 1, // Validate after every setValue()
OnRequest = 2, // Validate only when validate() is called
}

// Default: OnChange | OnRequest (bitwise OR)
<Form validationMode={ValidationMode.OnChange | ValidationMode.OnRequest} />

ShowError​

enum ShowError {
Dirty = 1, // show when node.dirty === true
Touched = 2, // show when node.touched === true
DirtyTouched = 3, // show when both dirty AND touched (default)
}

<Form showError={ShowError.DirtyTouched} /> // default
<Form showError={true} /> // always show
<Form showError={false} /> // never show

Error Structure​

interface JSONSchemaError {
dataPath: string; // JSONPointer to the failing field
keyword: string; // JSON Schema keyword (e.g., 'minLength', 'required')
message: string;
details: any; // keyword-specific details
source: any; // raw error from the validator library
}

Event System​

Nodes communicate through a batched event system. Events scheduled in the same synchronous call stack are merged into a single batch and dispatched in the next microtask. UpdateValue is the exception — it fires synchronously (immediate mode) after initialization completes.

// Key event types (NodeEventType enum)
UpdateValue; // value changed — synchronous after init
UpdateState; // dirty/touched/validated changed
UpdateError; // validation errors changed
UpdateComputedProperties; // computed visible/readOnly/disabled changed
UpdateChildren; // array/object children added or removed
RequestRefresh; // internal: sync uncontrolled component
RequestRemount; // external: force full component remount
AI Agent Reference

AI Concept Glossary​

TermDefinition
SchemaNodeBase interface for all nodes — the public API surface
StringNode / NumberNode / BooleanNode / NullNodeTerminal nodes — no children
ObjectNode / ArrayNodeBranch nodes — have children
VirtualNodeNon-schema node created for conditional branches (oneOf/anyOf)
BranchStrategyStrategy for complex nested structures with child nodes
TerminalStrategyStrategy for simple non-nested structures
FormTypeInputDefinition{ test, Component } pair — maps a schema pattern to a React component
FormTypeInputMap{ [jsonPointerPath]: Component } — path-based component override
FormTypeInputPropsProps interface all custom input components must satisfy
FormTypeRendererPropsProps for FormGroup/FormLabel/FormInput/FormError renderer components
SchemaFormPluginPlugin object with optional renderer components + formTypeInputDefinitions + validator + formatError
ValidatorPlugin{ bind(instance), compile(jsonSchema) → ValidateFunction }
ValidateFunction(value) => JSONSchemaError[] | null | Promise<...>
computed / &computedJSON Schema extension for reactive field properties
&ifJSON Schema extension — expression to activate a oneOf/anyOf branch
enhancedValueValue including virtual/conditional field values — used for validation
ShowErrorEnum controlling when validation errors are displayed
ValidationModeEnum controlling when validation runs
NodeEventTypeEnum of all events nodes can emit
SetValueOptionBitwise options for setValue() — Overwrite, Merge, Isolate
FormHandleRef interface for imperative form control
FormProviderContext provider for sharing plugin config across a subtree
JSONPointerRFC 6901 path string (e.g., /address/street) — extended with .., ., *