Architecture
How the Form Builder, Form Definition, Core, hooks, Renderer, Registry, and app code connect.
Schemaform is organized around one persisted boundary: the Form Definition. Everything else either creates that definition, interprets it, renders it, or submits data from it.
Admin user
-> Form Builder
-> Form Definition
{ formatVersion, schema, uiSchema, rules }
-> your storage
End user
-> Form Renderer
definition + Working State
-> Core
resolved view model + validation + submission projection
-> Registry
widget name -> concrete component
-> Submission Payload
-> your applicationThe important split is ownership. Schemaform owns the shared form language and the mechanics for resolving it. Your product owns persistence, routes, permissions, data fetching, analytics, side effects, and final submission handling.
Design-time flow
At design time, an admin-facing Form Builder edits a Form Definition.
Builder UI
-> builder hook
-> Core mutation functions
-> updated Form Definition
-> your save actionThe Form Builder edits three documents together:
| Document | Owns | Example |
|---|---|---|
| JSON Schema | data shape and validation | type, required, enum, minLength |
| UI Schema | presentation and layout | Pages, Groups, Columns, Controls, widgets |
| Rules | conditional behavior | show, hide, enable, disable, require |
The builder does not submit end-user data. Its output is configuration: a versioned Form Definition that your application stores.
Runtime flow
At runtime, a Form Renderer receives a Form Definition and renders it for an end user.
Form Definition
+ initial values
+ current Working State
-> useFormRenderer
-> Core resolver
-> FormViewModel
-> Renderer
-> widgetsThe Working State is the live data while the user fills out the form. It may include values for hidden fields, disabled fields, or fields on another Page.
Core evaluates Rules against the Working State, resolves the layout, derives widget names, and reports validation state. The Renderer turns that resolved model into concrete UI.
Submit flow
On submit, the Renderer should not send Working State directly. It should project a Submission Payload from the resolved view model.
Working State
+ FormViewModel
-> toSubmissionPayload
-> hidden fields stripped unless retained
-> final app submit handlerThis is where hidden-field behavior matters. A hidden field can remain in Working State so the user does not lose input while interacting with the form, but still be removed from the Submission Payload unless the Control or Array opts into retainWhenHidden.
Your app receives the payload through onSubmit and decides what happens next: call an API, save a draft, start a workflow, show a confirmation, or map the payload to another domain model.
Package responsibilities
The installed package exposes one default API, with advanced subpath entrypoints that mirror the internal layers.
| Layer | Responsibility | Inputs | Outputs |
|---|---|---|---|
schemaform/core | Pure form engine | Form Definition, Working State, rule directives | resolved form model, validation results, submission payload |
schemaform/react | React state boundary | definition, data, validators, navigation options | hooks for renderers and builders |
schemaform/shadcn | First-party UI implementation | hooks, registry, ShadCN/RHF components | <SchemaForm />, <FormBuilder />, widgets |
| Your app | Product integration | stored definitions, auth, routes, APIs | saved definitions, submitted data, product behavior |
Core is intentionally framework-agnostic. It does not render components, own input state, know about React Hook Form, or call your backend.
React hooks sit at the boundary between pure Core and stateful UI. They resolve forms, manage navigation, reveal errors, run custom validators, and expose stable helpers for renderers and builders.
The ShadCN package is a working renderer and builder implementation. It is both usable as-is and useful as source to copy when you want your own design-system-native implementation.
Renderer boundary
A Renderer owns mutable form state and concrete components.
Renderer owns:
- form library integration
- field registration
- focus and blur behavior
- concrete widgets
- layout markup
- submit button UX
Core owns:
- schema interpretation
- rule evaluation
- default widget derivation
- resolved Controls and Pages
- validation
- submission projectionThe Renderer should render from the resolved view model, not directly from raw JSON Schema. That resolved model is where Rules, UI Schema layout, required state, disabled state, default widgets, and hidden-field behavior have already been interpreted.
Registry boundary
A Registry maps widget names to concrete components.
Control
{ scope: "#/properties/startDate", widget: "date" }
-> resolved Control
-> registry.date
-> DateWidgetCore can decide that a string with format: "date" wants the date widget. The Renderer decides what date means visually and behaviorally in your application.
The same Registry also powers the Form Builder's widget palette. Adding a custom widget to the Registry makes it available to render and, in the ShadCN builder, available for admins to choose.
Trust boundary
A Form Definition is configuration, not arbitrary trusted code. It may come from your database, an admin interface, a seed file, or an import flow.
Treat it as a trust boundary:
- validate imported definitions before storing or rendering them
- keep custom behavior in app code, validators, widgets, renderers, and submit handlers
- avoid storing executable code inside definitions
- version definitions with the
formatVersionenvelope field
Definitions should describe data, presentation, and rules. Your application should own code execution.
Mental model
Use this rule when deciding where code belongs:
| Question | Put it in |
|---|---|
| What data can be submitted? | JSON Schema |
| How should the form be arranged? | UI Schema |
| What changes based on answers? | Rules |
| What component renders a field? | Registry |
| How does a component behave? | Widget code |
| How do values live in React? | Renderer or form library |
| How does validation call my backend? | Custom validators |
| Where is the form saved? | Your app |
| What happens after submit? | Your app |
Read The Three Layers for the modeling rules inside a Form Definition, then Extension Points for where to plug in custom product code.