Skip to content

Formhaus vs react-jsonschema-form ​

react-jsonschema-form (rjsf) builds a React form from a JSON Schema that describes your data, with a uiSchema for presentation; Formhaus uses a 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 a form for it with one of many UI themes, rjsf is the better choice. If you design the form as a flow with steps and branching, render it in Vue as well as React, or draw it in Figma, Formhaus fits that directly.

Choose react-jsonschema-form when ​

  • The data model is already a JSON Schema, from an API, OpenAPI spec or backend, and the form should follow it.
  • You need nested objects, arrays and oneOf choices generated from the schema (oneOf, anyOf and allOf).
  • You want a ready theme: Ant Design, Chakra UI, Fluent UI, Mantine, MUI, PrimeReact, React Bootstrap, Semantic UI or shadcn (themes).
  • Validation should be standard JSON Schema validation through Ajv (validation).

Choose Formhaus when ​

  • The form has steps. The rjsf docs have no step API. A closed 2017 issue discussed swapping the schema per step in your own code.
  • Fields appear based on answers with simple rules (eq, in, notEmpty) rather than JSON Schema dependencies and oneOf.
  • The app uses Vue, or the form has to be drawn in Figma.
  • AI agents write forms, and you want an MCP server that validates them and simulates every step path.

Feature comparison ​

Formhausreact-jsonschema-form
Definition formatForm definition JSON, checked by a JSON SchemaJSON Schema for data plus uiSchema for presentation (uiSchema)
Multi-stepBuilt in: steps, per-step validation, progress (guide)No step API in the docs
Branching routesroutes on a step (guide)No routing API in the docs
Conditional fieldsshow / showAny; hidden values cleared (guide)Schema dependencies, including oneOf for dynamic fields (dependencies)
ValidationDeclarative rules, named validators, async step validation (guide)JSON Schema via a validator package, customValidate, transformErrors, extraErrors for server errors (validation)
Custom componentscomponents map per field type (guide)Custom widgets, fields and templates (custom widgets and fields)
FigmaPlugin draws the definition (guide)Not documented
AI toolingJSON Schema, MCP server, Claude Code pluginInput is standard JSON Schema (docs); no AI tooling documented
FrameworksReact 18+, Vue 3.3+, headless engineReact 18+ (npm)
Bundle size (gzipped)6.2 KB core + 4.8 KB React50.6 KB @rjsf/core 6.11.0 (bundlephobia) + 38.2 KB @rjsf/validator-ajv8 6.11.0 (bundlephobia), plus the @rjsf/utils peer; other themes are separate packages
LicenseMITApache-2.0 (docs)

Sizes measured on 2026-10-09. See how Formhaus sizes are measured.

Migration ​

The same sign-up form: email, plan, and a company field only for the Team plan.

react-jsonschema-form ​

tsx
import Form from '@rjsf/core';
import validator from '@rjsf/validator-ajv8';
import type { RJSFSchema, UiSchema } from '@rjsf/utils';

const schema: RJSFSchema = {
  title: 'Sign up',
  type: 'object',
  required: ['email', 'plan'],
  properties: {
    email: { type: 'string', title: 'Email', pattern: '^\\S+@\\S+$' },
    plan: {
      type: 'string',
      title: 'Plan',
      oneOf: [
        { const: 'solo', title: 'Solo' },
        { const: 'team', title: 'Team' },
      ],
    },
  },
  dependencies: {
    plan: {
      oneOf: [
        { properties: { plan: { enum: ['solo'] } } },
        {
          properties: { plan: { enum: ['team'] }, company: { type: 'string', title: 'Company' } },
          required: ['company'],
        },
      ],
    },
  },
};

const uiSchema: UiSchema = {
  'ui:submitButtonOptions': { submitText: 'Create account' },
};

export function Signup({ onDone }: { onDone: (values: Record<string, unknown>) => void }) {
  return (
    <Form
      schema={schema}
      uiSchema={uiSchema}
      validator={validator}
      onSubmit={({ formData }) => onDone(formData)}
    />
  );
}

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 type { FormDefinition } from '@formhaus/core';
import signup from './signup.json';

const definition = signup as FormDefinition;

export function Signup({ onDone }: { onDone: (values: Record<string, unknown>) => void }) {
  return <FormRenderer definition={definition} onSubmit={onDone} />;
}

properties become fields, required moves into each field's validation, and the dependencies block becomes one show condition on company. Error messages live next to each rule instead of in transformErrors. Nested objects and arrays have no Formhaus equivalent; keep those forms on rjsf.