JSON SCHEMA · REACT · TYPESCRIPT

Define forms as data.
Own the code that renders them.

Schemaform is a headless React toolkit built on JSON Schema. Install the package, use the default renderer, and fork the source only when your product needs deeper control.

$ npm install @mackehansson/schemaform
contact.form.json
{
  "type": "object",
  "required": ["email", "plan"],
  "properties": {
    "email": { "type": "string", "format": "email" },
    "plan":  { "enum": ["Free", "Pro", "Enterprise"] },
    "seats": { "type": "integer", "minimum": 1 }
  }
}
<SchemaForm />
Rendered formlive
form.value
{"email":"","plan":"Free"}
01 — THE MODEL

One definition, three distinct layers.

A form is data, presentation, and behavior — kept separate. Schemaform never mixes validation into your markup or hard-codes layout into your logic.

JSON Schema

The data shape and its validation. The single source of truth for what's valid — standard JSON Schema, no proprietary dialect.

{ "type": "string",
  "format": "email" }
UI Schema

Presentation and layout. Which widget renders each field, labels, grouping, order — all without touching the data contract.

{ "widget": "select",
  "label": "Plan" }
Rules

Conditional behavior. Show, hide, enable, or require fields based on other answers — evaluated by the core, not your components.

{ "when": { … },
  "show": ["seats"] }
02 — WHY HEADLESS

Install first. Copy-friendly by design.

Use the package directly, then browse or copy the source when your product needs to own a renderer, widget, or builder workflow.

Readable source

Start with the installed package. When you need deeper control, copy the renderer or widgets you want to own.

Swap the renderer

Ship with the first-party ShadCN renderer or wire the hooks to any component stack you already use.

{ }

Headless hooks

useFormRenderer() hands you state, validation, and rule evaluation. UI is entirely your call.

Framework-agnostic core

Validation and rules run in a plain TypeScript core. React hooks are a thin layer on top.

Drag-and-drop builder

A visual builder emits the same definition your renderer consumes. No export step, no translation.

JSON Schema standard

Definitions are portable JSON Schema — validate them anywhere, store them in any database, version them in git.

03 — TWO SURFACES

One definition powers both sides.

Admins design a form in the Builder. End users fill it out in the Renderer. Both read the exact same definition envelope — there's nothing to keep in sync.

app.acme.com/admin/forms/contact
FIELD TYPES
Text
Select
Number
Checkbox
Date
Work email
Planselected
select ▾
+ Drop a field here
INSPECTOR · plan
Field key
plan
Widget
select
Options
FreeProEnterprise
Required
04 — DEVELOPER EXPERIENCE

Author forms with typed helpers.

Write the form once with defineForm, field, layout helpers, and rule helpers. Schemaform returns the same persisted Form Definition envelope, but developers get field-key suggestions and fewer raw JSON pointers.

Typed field keys

Pages and rules are inferred from the fields object, so editors can suggest keys like email or plan.

Declarative rules

Use rule.when("plan").equals("Enterprise").require("companyName") instead of hand-writing JSONLogic.

Plain output

The renderer, builder, parser, and migrations still consume the normal Form Definition shape.

SignupDefinition.tstyped authoring
import { defineForm, field } from "@mackehansson/schemaform";

const signupForm = defineForm({
  fields: {
    email: field.email({ title: "Email", required: true, meta: { iconText: "Use work email" } }),
    plan: field.select({ title: "Plan", options: ["Free", "Enterprise"] }),
    companyName: field.text({ title: "Company" }),
  },
  pages: ({ page }) => [
    page("Signup", ["email", "plan", "companyName"]),
  ],
  rules: ({ rule }) => [
    rule.when("plan").equals("Enterprise").require("companyName"),
  ],
});
05 — IN PRACTICE

From helper definition to rendered form.

Import defineForm, field, and SchemaForm from the main package. The helper returns a normal Form Definition, and the bundled renderer handles validation, rules, and submit projection.

ContactForm.tsx
import { defineForm, field, SchemaForm } from "@mackehansson/schemaform";

const contactForm = defineForm({
  fields: {
    name: field.text({ title: "Name", required: true }),
    email: field.email({ title: "Email", required: true }),
  },
  pages: ({ page }) => [
    page("Contact", ["name", "email"]),
  ],
});

export function ContactForm() {
  return (
    <SchemaForm
      definition={contactForm}
      onSubmit={(value) => createLead(value)}
    />
  );
}
RENDERS
Work email
Plan
Pro
Submit

No wiring. Validation, rules, and submit handling come from the hook.

06 — WHO IT'S FOR

Infrastructure you own. Not a SaaS you rent.

For React teams building internal tools and SaaS product that need user-designed, dynamic forms — where the form data and the code that renders it both have to stay yours.

Hosted form SaaS
Schemaform
Where your data lives
Vendor's servers
Your infrastructure
The rendering code
Closed, hosted
Yours, in your repo
Custom field types
Limited / paid tier
Any React component
Runtime model
Hosted dependency
Runs in your app
Lock-in
A migration project
It's just your code
Offline / on-prem
Rarely
Always — it runs in your app

Stop renting your forms.

Install the package, render a Form Definition, and keep the option to inspect or fork the source when your product needs deeper control.

Get started →View source
$ npm install @mackehansson/schemaform