JSON Schema form
JsonSchemaForm maps an object schema to a controlled React form. It handles common JSON Schema structure itself and exposes pre and post pipelines for application-specific widgets.
Basic form
Section titled “Basic form”import { useState } from "react";import { JsonSchemaForm, type JsonSchemaObject,} from "@flanksource/clicky-ui/components";
const schema: JsonSchemaObject = { type: "object", required: ["name", "enabled"], properties: { name: { type: "string", title: "Display name", description: "A human-readable name for this connection.", }, enabled: { type: "boolean", default: true }, provider: { type: "string", enum: ["aws", "azure", "gcp"], "x-enum-labels": { aws: "Amazon Web Services", azure: "Microsoft Azure", gcp: "Google Cloud", }, }, },};
export function ConnectionForm() { const [value, setValue] = useState<Record<string, unknown>>({ enabled: true, }); return ( <JsonSchemaForm schema={schema} value={value} onChange={setValue} idPrefix="connection" /> );}The form does not apply defaults to value; initialize controlled state yourself when defaults need to be present before the user edits a field.
Supported JSON Schema behavior
Section titled “Supported JSON Schema behavior”The renderer consumes the following standard schema features:
- Primitive
typevalues:string,number,integer,boolean,null,array, andobject. - Metadata and constraints:
title,description,default,readOnly,enum,const,minimum,maximum, andmultipleOf. - Nested data:
properties,required,items,additionalProperties,patternProperties, andpropertyNames.enumfor constrained map keys. - Composition: local
#/$defs/...references, unconditionalallOfmembers, andallOfclauses usingif/thenconditionals. An enum insideanyOforoneOfcan supply choices for a value-or-free-text union. - String formats:
date,date-time,textarea,md, andpercent.
This is a rendering contract, not a complete JSON Schema validator. Unknown keywords pass through so consumer extensions can interpret them, and change handlers are not blocked by validation hints.
Layout and display preferences
Section titled “Layout and display preferences”Use layout for an explicit form-wide arrangement or inline as the short form:
layout.mode |
Presentation | Value updates |
|---|---|---|
stacked |
Labels above controls; supports schema-defined column grids. | Each control edit calls onChange. |
inline |
Aligned label/value columns, with width caps. | Each control edit calls onChange. |
properties |
Property rows with indented nested labels and aligned value previews. | Click a value to edit a draft; Save commits it and Cancel discards it. |
<JsonSchemaForm schema={schema} value={value} onChange={setValue} size="sm" layout={{ mode: "inline", labelMaxWidth: "28ch", valueMaxWidth: "48rem", }}/>The preferences menu is visible by default and lets a user change this form’s size, layout, sort order, and top-level field filter. Preferences share the clicky-ui-json-schema-form-preferences local-storage key unless preferencesStorageKey is set. Set persistPreferences={false} for instance-only choices, or showPreferencesMenu={false} to remove the menu and all preference storage access.
Menu choices override the initial size and layout props. Disable the menu when a screen must keep a fixed layout. Field-level x-layout and x-label-position hints control stacked/inline rendering; property mode keeps fields in property rows. Set layout.help: "hover" to move descriptions behind a help affordance, or use x-help-display on an individual field.
Property rows
Section titled “Property rows”<JsonSchemaForm schema={schema} value={value} onChange={setValue} layout={{ mode: "properties" }} showPreferencesMenu={false}/>Fixed nested objects expand into indented rows. Arrays and maps show a count until opened for editing, where they use the existing schema controls. Saving a nested value rebuilds the root object through onChange. A field marked readOnly, or a form with readOnly={true}, displays values without edit buttons. Use hideReadOnlyFields to omit schema-declared read-only fields entirely.
Try the property-row example to compare Save and Cancel behavior.
Server validation errors
Section titled “Server validation errors”Pass authoritative validation results through errors. Each error uses an RFC 6901 JSON Pointer into the root value; array indices are path segments, for example /servers/0/host.
<JsonSchemaForm schema={schema} value={value} onChange={setValue} errors={[ { instancePath: "/name", keyword: "required", message: "Enter a name." }, { instancePath: "", message: "Review the connection before saving." }, ]}/>Exact field matches appear beside their controls. Root errors and paths that do not match a rendered field appear in the form summary. The host owns validation, error updates, and submission decisions; supplying errors does not block onChange.
Application extensions
Section titled “Application extensions”A pre extension transforms a resolved FieldControl before it renders, or returns null to omit it. Extensions compose in array order and run recursively at every nesting depth.
import type { PreExtension } from "@flanksource/clicky-ui/components";
const emphasizeSecrets: PreExtension = (field) => { if (field.schema.format !== "password") return field; return { ...field, badge: "secret", inputClassName: "font-mono", };};
<JsonSchemaForm {...props} pre={[emphasizeSecrets]} />;A post extension receives the final label and value nodes and can wrap or replace them. Both extension types can read the top-level form value, which is useful for sibling-dependent widgets. Keep domain behavior in these extension functions instead of adding field-name heuristics to the shared renderer.
Async lookups
Section titled “Async lookups”x-clicky-lookup turns a string into a searchable entity picker. The schema describes the request and the host supplies transport through lookupFetcher:
const schema: JsonSchemaObject = { type: "object", properties: { connection: { type: "string", "x-clicky-lookup": { url: "/api/v1/connections", filter: "id", scope: { param: "type", from: "provider.type" }, }, }, },};
<JsonSchemaForm schema={schema} value={value} onChange={setValue} lookupFetcher={async ({ descriptor, query, rootValue }) => { return api.lookup({ descriptor, query, rootValue }); }}/>;See the extension reference for every built-in x-* key and its exact values.