Formhaus vs JSON Forms
JSON Forms renders a form from a JSON Schema that describes your data and a UI schema that lays out the controls. Formhaus uses one form definition that describes the form itself: fields, steps, routes and conditions. If your data model already exists as a JSON Schema and you want Angular, Material UI or deeply nested data, JSON Forms is the better choice. If the form is a flow with steps and branches, renders in React and Vue, is drawn in Figma or is written by an AI agent, Formhaus fits that directly.
Choose JSON Forms when
- The data model is already a JSON Schema, from an API, OpenAPI spec or backend, and the form should follow it (UI schema).
- The app is in Angular, or you want Material UI controls out of the box (renderer sets).
- The data has arrays of objects, each item rendered with its own nested UI schema (controls). Formhaus groups do not nest.
- Validation should be standard JSON Schema validation through AJV, with backend errors passed in as
additionalErrors(validation).
Choose Formhaus when
- The form has steps. JSON Forms shows a
Categorizationas tabs, or as a stepper with"variant": "stepper"(example). The stepper is in React Material, Vue Vanilla and Vue Vuetify; React Vanilla shows a category list and Angular Material shows tabs.showNavButtonsadds Back and Next buttons, and Next does not validate the step (source). Formhaus validates each step on Continue and has skip, progress and a welcome screen (guide). - The next step depends on an answer. Formhaus has
routeson a step (guide). JSON Forms can hide a category with a rule, and has no API that picks the next category (docs). - Conditions should be short:
"show": [{ "field": "plan", "eq": "team" }]instead of ascopeand a JSON Schema per rule (rules). - You want one file. Labels, order, steps and validation messages live in the definition, not in a schema and a UI schema.
- The form is drawn in Figma, or AI agents write it and you want an MCP server that checks it and walks every path.
Feature comparison
| Formhaus | JSON Forms | |
|---|---|---|
| Definition format | One form definition, checked by a JSON Schema | JSON Schema for data plus a UI schema for layout (UI schema) |
| Layout | Steps, sections, matrix sections, groups (guide) | VerticalLayout, HorizontalLayout, Group, Categorization (layouts) |
| Multi-step | Built in, with per-step validation, skip and progress (guide) | Categorization as tabs or a stepper (example); Next does not validate (source) |
| Branching routes | routes on a step (guide) | Not in the docs |
| Conditional fields | show and showAny with eq, neq, in, notIn, contains, notContains, notEmpty, gt, gte, lt, lte (guide) | Rules with SHOW, HIDE, ENABLE, DISABLE; the condition is a JSON Schema checked against a scope (rules) |
| Repeatable items | Groups, which do not nest (guide) | Arrays and nested objects from the schema (controls) |
| Validation | Declarative rules, named validators, async step validation, validateSubmission() on the server (guide) | JSON Schema through AJV, additionalErrors from the backend (validation) |
| Submit | Built-in actions and onSubmit | No submit button; read data and errors in onChange (React) |
| Custom components | components map per field type, headless renderer, useFormEngine (guide) | Custom renderers picked by testers (tutorial) |
| Frameworks | React 18 and 19, Vue 3.3.3+, framework-agnostic engine | React 16.12 to 19 (peers), Angular 20 to 22 (peers), Vue 3.5+ (peers) |
| Renderer sets | Built-in fields, or your own components | React Material, React Vanilla, Angular Material, Vue Vanilla, Vue Vuetify in preview (renderer sets) |
| i18n | Message and label catalogs, translation keys (guide) | translate function and keys derived from the property path (i18n) |
| Figma | Plugin draws the definition (guide) | Not documented |
| AI tooling | JSON Schema, MCP server, Claude Code plugin | Input is standard JSON Schema; no AI tooling documented |
| Bundle size (gzipped) | 33.5 KB core + React | 82.6 KB for @jsonforms/core + @jsonforms/react + @jsonforms/vanilla-renderers 3.8.0 (how it's measured) |
| License | MIT | MIT (license) |
In JSON Forms 3.8.0, AJV and lodash are two thirds of the minified bundle.
Migration
The same sign-up form: email, plan, and a company field only for the Team plan.
JSON Forms
import { JsonForms } from '@jsonforms/react';
import { vanillaCells, vanillaRenderers } from '@jsonforms/vanilla-renderers';
import { useState } from 'react';
const schema = {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
plan: { type: 'string', enum: ['solo', 'team'] },
company: { type: 'string' },
},
required: ['email', 'plan'],
if: { properties: { plan: { const: 'team' } } },
then: { required: ['company'] },
};
const uischema = {
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/email', label: 'Email' },
{ type: 'Control', scope: '#/properties/plan', label: 'Plan' },
{
type: 'Control',
scope: '#/properties/company',
label: 'Company',
rule: { effect: 'SHOW', condition: { scope: '#/properties/plan', schema: { const: 'team' } } },
},
],
};
export function Signup({ onDone }: { onDone: (values: Record<string, unknown>) => void }) {
const [data, setData] = useState({});
const [errors, setErrors] = useState<unknown[]>([]);
return (
<>
<JsonForms
schema={schema}
uischema={uischema}
data={data}
renderers={vanillaRenderers}
cells={vanillaCells}
onChange={({ data, errors }) => {
setData(data);
setErrors(errors ?? []);
}}
/>
<button type="button" disabled={errors.length > 0} onClick={() => onDone(data)}>
Create account
</button>
</>
);
}Formhaus
{
"id": "signup",
"title": "Sign up",
"submit": { "label": "Create account" },
"fields": [
{
"key": "email",
"type": "email",
"label": "Email",
"validation": {
"required": "Enter your email",
"pattern": "^\\S+@\\S+$",
"patternMessage": "Enter a valid email"
}
},
{
"key": "plan",
"type": "select",
"label": "Plan",
"options": [{ "value": "solo", "label": "Solo" }, { "value": "team", "label": "Team" }],
"validation": { "required": true }
},
{
"key": "company",
"type": "text",
"label": "Company",
"show": [{ "field": "plan", "eq": "team" }],
"validation": { "required": "Enter your company" }
}
]
}import { FormRenderer } from '@formhaus/react';
import { parseDefinition } from '@formhaus/core';
import signup from './signup.json';
const definition = parseDefinition(signup);
export function Signup({ onDone }: { onDone: (values: Record<string, unknown>) => void }) {
return <FormRenderer definition={definition} onSubmit={onDone} />;
}The schema and the UI schema become one fields list. required moves into each field's validation. The if/then block and the SHOW rule become one show condition on company. The submit button and the error check are built in. Arrays of objects become a group. Arrays nested inside them have no Formhaus equivalent; keep those forms on JSON Forms.