@texaryn/core
v0.13.0
Published
Headless runtime for JSON Schema forms and schema-driven UI: a framework-neutral UI IR, compiler, form state and renderer registry, used with @texaryn/schema-json and a React, Vue or Web Components binding
Maintainers
Readme
@texaryn/core
Headless runtime for JSON Schema forms and schema-driven UI: a framework-neutral UI IR, compiler, form state and renderer registry, used with @texaryn/schema-json and a React, Vue or Web Components binding.
Status: pre-1.0. Public APIs may change before 1.0.
Install
pnpm add @texaryn/core@texaryn/core has no React, Vue or DOM dependency.
What it does
Texaryn keeps schema evaluation, UI structure, runtime state, and rendering separate:
SchemaEvaluationPort
|
v
SchemaProjection
|
v
compile()
|
v
UIDocument
|
v
FormRuntime
|
v
RendererThe core package owns the framework-neutral contracts and deterministic runtime. A schema adapter supplies semantic information through SchemaEvaluationPort, and a renderer consumes the resulting UI document and runtime state.
Quick start
Most applications use @texaryn/core together with a schema adapter and a renderer.
import { createFormRuntime } from '@texaryn/core'
import { createJsonSchemaAdapter } from '@texaryn/schema-json'
const schema = {
type: 'object',
properties: {
name: { type: 'string', title: 'Name' },
},
required: ['name'],
}
const adapter = await createJsonSchemaAdapter(schema)
const runtime = createFormRuntime(adapter, {
initialData: { name: '' },
onSubmit(data) {
console.log(data)
},
})
const document = runtime.document.getSnapshot()
const data = runtime.data.getSnapshot()
console.log(document.rootId, data)
runtime.dispatch({ type: 'Submit' })
runtime.destroy()Schema evaluation contract
Schema libraries integrate with Texaryn through SchemaEvaluationPort:
interface SchemaEvaluationPort {
project(data: unknown): SchemaProjection
validate(data: unknown): ValidationResult | Promise<ValidationResult>
validateAt?(
data: unknown,
pointer: JsonPointer,
): ValidationResult | Promise<ValidationResult>
}The core does not parse or validate JSON Schema itself. That responsibility belongs to schema adapter packages such as @texaryn/schema-json.
Runtime
Create a runtime with:
import { createFormRuntime } from '@texaryn/core'
const runtime = createFormRuntime(port, {
initialData,
hints,
onSubmit,
})A FormRuntime exposes stores for:
document, the currentUIDocumentdata, the current form datasubmission, the current submission statevisibleErrors, every error currently shown across the form
It also exposes:
dispatch(command)getNodeState(nodeId)destroy()
Node state is kept outside the UI IR and includes value, validation, dirty/touched state, visibility, and disabled state.
Read-only is not node state: it comes from the schema, so it lives on the
compiled node as readOnly and is resolved rather than raw. A read-only object
or array makes everything beneath it read-only too, because editing a
descendant changes the ancestor's value. The runtime refuses SetValue,
InsertItem, RemoveItem and MoveItem on a read-only node, so a renderer
cannot write past it. Reset is deliberately exempt: it is the owning
authority replacing state wholesale rather than a user edit, and it is how a
server-owned value legitimately changes.
Commands
The runtime currently understands:
{ type: 'SetValue', nodeId, value }
{ type: 'InsertItem', containerId, index, value? }
{ type: 'RemoveItem', containerId, index }
{ type: 'MoveItem', containerId, from, to }
{ type: 'SetTouched', nodeId }
{ type: 'Submit' }
{ type: 'Reset', data? }Commands are data. They do not contain renderer or DOM behavior.
UI IR
compile() converts a SchemaProjection into a flat, versioned UIDocument:
interface UIDocument {
version: 1
rootId: NodeId
nodes: Record<NodeId, UINode>
}The IR contains semantic nodes such as fields, containers, text, and actions. It describes what the UI means, not how a framework should render it.
UI hints
Presentation can be layered on top of schema semantics:
const hints = {
'/email': {
placeholder: '[email protected]',
order: 1,
},
'/bio': {
widget: 'textarea',
helpText: 'Tell us about yourself.',
order: 2,
},
}Hints can also control when validation runs:
const hints = {
'/email': { validationTrigger: 'blur' },
'/search': { validationTrigger: 'change' },
}Three triggers are supported: blur validates on field blur (SetTouched), change validates after a debounce on value changes (SetValue, array mutations), and submit validates only on form submission. Fields without a validationTrigger hint are not automatically validated on blur or change; submit always validates the entire form regardless of hints.
The change debounce defaults to 300ms and can be configured:
const runtime = createFormRuntime(port, {
initialData,
hints,
validationDebounceMs: 500,
})Validation remains full-form: port.validate(data) is called with the entire form data, and errors are distributed to nodes by their data pointers. Synchronous validators skip the pending state entirely.
Errors are not displayed immediately. The runtime computes a showErrors flag per node: a field's errors become visible once it is invalid and either touched (the user has interacted with it) or the form has been submitted, because a failed Submit has to show what blocked it, including fields the user never reached. submission.attempts counts the accepted Submit commands since creation or the last Reset, and the gate stays open while it is above zero. The form-level visibleErrors store aggregates all currently visible errors for use in error summaries. The useField hook in both @texaryn/react and @texaryn/vue returns showErrors, and the React getInputProps sets aria-invalid and the error ID in aria-describedby only when showErrors is true.
Pass hints to createFormRuntime(), or to useForm() in React or Vue.
Submission lifecycle
Dispatching Submit captures the current form data as an immutable snapshot and triggers full-form validation against it. If validation passes, the runtime transitions to submitting and calls onSubmit with the captured data:
const runtime = createFormRuntime(port, {
initialData: { name: '' },
onSubmit: async (data) => {
await api.save(data)
},
})
runtime.dispatch({ type: 'Submit' })The submission store tracks the lifecycle: idle, validating, submitting, submitted. It also carries attempts, the number of accepted Submit commands since creation or the last Reset. A failed validation returns to idle with every invalid field showing its errors; a failed onSubmit returns to idle with the error on submission.error. Dispatching Submit while already validating or submitting is a no-op.
Edits during validating cancel the submission attempt and return to idle. Edits during submitting update the live form data but do not alter the in-flight payload or trigger validation. A Reset during submission cleanly cancels via a generation counter, so late completions from abandoned attempts are ignored.
Renderer registry
The core exports a framework-neutral renderer registry:
import { createRendererRegistry } from '@texaryn/core'
const registry = createRendererRegistry()Framework packages can register concrete widget components without adding framework dependencies to the runtime.
Stable array identity
The core includes identity helpers used to keep dynamic array items stable across inserts, removals, moves, and recompilation. Arrays are addressed by ArrayMeta.identityKey, an opaque key for the logical container that stays the same when rows above it move or change shape, so a nested array keeps its item identities when its parent row moves.
Identity follows structural edits: InsertItem, RemoveItem and MoveItem keep the ids of every item they do not touch, including the nested arrays inside a moved row. Reset is wholesale state replacement rather than a structural edit: it re-establishes identity by matching old and new items (by ArrayMeta.itemKey when set, otherwise by content), so the nested arrays under a row that changed position may be minted afresh.
The helpers:
createIdentityMapregisterArrayinsertItemremoveItemmoveItemresolvePointerreconcile
Key exports
Core types
NodeIdStableItemIdJsonPointerMaybePromiseJsonSchemaTypeValidationResultValidationErrorVisibleError
Schema and IR
SchemaEvaluationPortSchemaProjectionNodeProjectionChildProjectionAnnotationSetUIDocumentUINodeNodeBaseNodeAnnotationsFieldNodeFieldTypeFieldConstraintsEnumOptionContainerNodeArrayMetaTextNodeActionNodeCompileResultcompile
UI hints
UIHintsFieldHintsArrayHints
Messages
FormMessages,ActionMessage,IndicatorMessage,ItemActionContext,AddItemContextenglishMessagesmergeMessages
Runtime and state
createFormRuntimeFormRuntimeFormRuntimeOptionsNodeStateRuntimeStateNodeRuntimeStateValidationStateInteractionStateSubmissionStateIdentityMapcreateStoreStoreWritableStore
Commands
CommandEffectCommandResultprocessCommand
JSON Pointer helpers
getAtPointersetAtPointerparsePointer
setAtPointer states its contract in full on the function itself: own members
only, what an absent or null level becomes, which tokens address an array
element, and which writes are refused rather than guessed. getAtPointer is
total and returns undefined for anything it cannot address.
Stable array identity
createIdentityMapregisterArrayinsertItemremoveItemmoveItemresolvePointerreconcileReconcileOptionsidentityKeyROOT_IDENTITY_KEYIdentityKeyIdentitySegment
Rendering
createRendererRegistryRendererRegistryWidgetTesterWidgetEntry
Design constraints
@texaryn/core intentionally does not depend on:
- React, Vue or any other rendering framework
- the DOM
- a JSON Schema implementation
- an AI SDK or transport protocol
That boundary is what allows the same runtime model to support different schema engines and renderers.
Related packages
@texaryn/schema-json, primary JSON Schema adapter@texaryn/react, React bindings and default renderer@texaryn/vue, Vue 3 bindings and default renderer
See the repository README and ROADMAP for the full architecture.
License
Apache-2.0
