Texaryn: schema-driven forms with a headless runtime
Published Updated
Texaryn turns schemas into forms while keeping form behavior separate from the framework that renders them. It also renders standalone display documents: lists, tables, layouts, text and actions handled by the host application, without editable form fields.
A schema adapter reads the schema. A headless TypeScript runtime owns state, validation and submission. A binding renders the result in React, Angular, Vue, Svelte, SolidJS or Web Components. The application keeps control of its components and design system.
Why I built it
I’ve maintained AJSF, the Angular JSON Schema form library, since 2018. It continues dschnelldavis/angular2-json-schema-form in its own repository: the first commit, in July 2018, brought that code to Angular 6. Its major version follows the Angular major it targets, and 22.2.1 is on Angular 22.
AJSF already keeps the schema logic apart from the design system (Material, Bootstrap, PrimeNG), but all of it runs inside Angular. Texaryn grew out of that, and out of exploring what AI needs to build interfaces: describe the interface as structured data, let a runtime own state and validation, and keep rendering in a thin layer per framework.
In RJSF, Form processes the schema, manages state and renders, all in React. I wanted three separate owners: an adapter that understands JSON Schema, a runtime that owns form state, and a binding that renders a framework-neutral description of the form.
The first user I had in mind builds form-heavy React apps from JSON Schema: configuration screens, multi-step onboarding, admin panels, data entry tools. They want per-field dirty and touched state, validation timing per field, and custom components without forking a theme, in React today without being locked into React. Vue was the second binding on purpose: setup runs once and reactivity is tracked per ref, where React re-runs a component on every render, so it shows whether the binding contract quietly assumed React’s model.
How the pieces fit together
JSON Schema, TypeBox or Zod
↓ schema adapter
SchemaProjection
↓ compiler
UI document
↓ form runtime
Framework binding
↓ widgets from a registry
Rendered form
createJsonSchemaAdapter implements SchemaEvaluationPort, the interface the
runtime calls to project and validate data. A projection describes the fields,
constraints and active branches for the current data, so it changes as the user
edits. The compiler turns it into a typed UI document.
The renderer receives that document rather than the schema. Conditionals,
composition and dependencies stay in the adapter; a widget does not need to
reimplement their semantics. FormRuntime owns values, validation, dirty and
touched state, submission and array identity. Bindings subscribe to it instead
of maintaining another copy of the data.
A registry maps UI nodes to components. Every binding includes default widgets; React also has Bootstrap and Material UI widget packages. The styling guide explains how to use those registries or style the default controls.
The core imports no rendering framework or AI SDK. The UI definition is data,
with no generated JavaScript or eval(). The
architecture guide
describes the package boundaries and their tests.
A working React form
Start in an existing React TypeScript application. The binding supports React 18.3 and React 19.
pnpm add @texaryn/core @texaryn/schema-json @texaryn/react
Replace src/main.tsx with this example. The page needs a
<div id="root"></div>, as in a Vite React template.
import { createRoot } from 'react-dom/client'
import { createJsonSchemaAdapter } from '@texaryn/schema-json'
import {
ErrorSummary, FormProvider, FormRoot, createDefaultRegistry, useForm,
} from '@texaryn/react'
async function main() {
const adapter = await createJsonSchemaAdapter({
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
title: 'Profile',
required: ['name'],
properties: {
name: { type: 'string', title: 'Name', minLength: 2 },
newsletter: { type: 'boolean', title: 'Subscribe to newsletter' },
},
})
const registry = createDefaultRegistry()
function App() {
const form = useForm(adapter, {
initialData: { name: '', newsletter: false },
onSubmit: (data) => console.log('Submitted', data),
})
return (
<FormProvider value={form.runtime}>
<form noValidate onSubmit={(event) => {
event.preventDefault()
form.dispatch({ type: 'Submit' })
}}>
<ErrorSummary />
<FormRoot registry={registry} />
<button type="submit">Submit</button>
</form>
</FormProvider>
)
}
createRoot(document.getElementById('root')!).render(<App />)
}
void main()
Submit an empty name and the error summary takes focus and links to the field.
Enter at least two characters and submit to log the data in the browser
console. Replace onSubmit with the application’s save function when connecting
a backend; this example sends no network request.
FormRoot renders the fields, not a native form. The application owns <form>,
prevents its default navigation, and dispatches Submit. That also lets Enter
submit. noValidate leaves validation to Texaryn.
Presentation hints stay separate from the data contract. To validate the name
on blur, add this to the useForm options:
hints: {
'/name': { placeholder: 'Your full name', validationTrigger: 'blur' },
},
The key is a JSON Pointer into the data. The hint changes when validation runs;
the schema’s minLength still defines what counts as valid. The
validation guide
covers error visibility, pending submissions and failed requests. Built-in
messages can be localized
without replacing the widgets.
Use the framework and schema you already have
The package chooser links to setup for React, Angular, Vue, Svelte, SolidJS and Web Components. Their component APIs follow their own frameworks, while the runtime contracts stay shared.
JSON Schema users install @texaryn/schema-json. TypeBox emits JSON Schema and
uses that adapter too. Zod users install @texaryn/schema-zod with
Zod 4. Zod validates the
input, while its JSON Schema conversion supplies field projection. Refinements
and transforms do not become projected field constraints. The
adapter guide
explains that boundary.
The JSON Schema adapter supports Draft 4, Draft 7, 2019-09 and 2020-12. Validation and form generation have different limits. Homogeneous arrays have generated controls; tuples do not have a dedicated projection. Static external references use a host-provided resolver, and dynamic reference projection is opt in for supported paths. Texaryn does not fetch schemas itself.
The validation compatibility page reports the test-suite results by adapter and dialect. Those results measure validation, not whether each keyword creates a control. Check the support guide against the schemas your application uses.
Display documents and model-generated interfaces
A form uses a version 1 UI document and FormRuntime. A standalone display
uses a version 2 display document
and createDocumentRuntime, without a schema adapter. These are different
document families, not an upgrade path for existing forms. Host actions can
perform operations, so a display without editable fields is not necessarily
read-only.
The AI generation contract describes structured output a model can produce for either family. Model calls stay in the application. Texaryn provides contracts, runtime validation and examples rather than a model service. The host still owns input limits, allowed widgets and actions, and authorization.
Compatibility and adoption
The stable contracts cover @texaryn/core,
@texaryn/schema-json and
@texaryn/react, each at
1.0. Other packages
version independently below 1.0 and may introduce breaking changes in minor
releases. The roadmap records the
compatibility boundary and product direction.
For an existing RJSF application, start with the
migration guide.
initialData initializes a runtime; it is not a controlled formData prop.
Keep autosave and parent-state synchronization by observing the data store,
and treat Reset as loading a new baseline rather than echoing each edit.
Try your schema in the playground, read the getting-started guide, or open an issue when an integration needs a contract the runtime does not expose.