Skip to content
Open Cloud

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 Categorization as 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. showNavButtons adds 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 routes on 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 a scope and 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 ​

FormhausJSON Forms
Definition formatOne form definition, checked by a JSON SchemaJSON Schema for data plus a UI schema for layout (UI schema)
LayoutSteps, sections, matrix sections, groups (guide)VerticalLayout, HorizontalLayout, Group, Categorization (layouts)
Multi-stepBuilt in, with per-step validation, skip and progress (guide)Categorization as tabs or a stepper (example); Next does not validate (source)
Branching routesroutes on a step (guide)Not in the docs
Conditional fieldsshow 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 itemsGroups, which do not nest (guide)Arrays and nested objects from the schema (controls)
ValidationDeclarative rules, named validators, async step validation, validateSubmission() on the server (guide)JSON Schema through AJV, additionalErrors from the backend (validation)
SubmitBuilt-in actions and onSubmitNo submit button; read data and errors in onChange (React)
Custom componentscomponents map per field type, headless renderer, useFormEngine (guide)Custom renderers picked by testers (tutorial)
FrameworksReact 18 and 19, Vue 3.3.3+, framework-agnostic engineReact 16.12 to 19 (peers), Angular 20 to 22 (peers), Vue 3.5+ (peers)
Renderer setsBuilt-in fields, or your own componentsReact Material, React Vanilla, Angular Material, Vue Vanilla, Vue Vuetify in preview (renderer sets)
i18nMessage and label catalogs, translation keys (guide)translate function and keys derived from the property path (i18n)
FigmaPlugin draws the definition (guide)Not documented
AI toolingJSON Schema, MCP server, Claude Code pluginInput is standard JSON Schema; no AI tooling documented
Bundle size (gzipped)33.5 KB core + React82.6 KB for @jsonforms/core + @jsonforms/react + @jsonforms/vanilla-renderers 3.8.0 (how it's measured)
LicenseMITMIT (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 ​

tsx
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 ​

json
{
  "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" }
    }
  ]
}
tsx
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.