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
oneOfchoices 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 SchemadependenciesandoneOf. - 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
| Formhaus | react-jsonschema-form | |
|---|---|---|
| Definition format | Form definition JSON, checked by a JSON Schema | JSON Schema for data plus uiSchema for presentation (uiSchema) |
| Multi-step | Built in: steps, per-step validation, progress (guide) | No step API in the docs |
| Branching routes | routes on a step (guide) | No routing API in the docs |
| Conditional fields | show / showAny; hidden values cleared (guide) | Schema dependencies, including oneOf for dynamic fields (dependencies) |
| Validation | Declarative rules, named validators, async step validation (guide) | JSON Schema via a validator package, customValidate, transformErrors, extraErrors for server errors (validation) |
| Custom components | components map per field type (guide) | Custom widgets, fields and templates (custom widgets and fields) |
| Figma | Plugin draws the definition (guide) | Not documented |
| AI tooling | JSON Schema, MCP server, Claude Code plugin | Input is standard JSON Schema (docs); no AI tooling documented |
| Frameworks | React 18+, Vue 3.3+, headless engine | React 18+ (npm) |
| Bundle size (gzipped) | 6.2 KB core + 4.8 KB React | 50.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 |
| License | MIT | Apache-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
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
{
"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 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.