How do we compare to other JSON form engines?
| Capability | GolemUI | JSON Forms | RJSF | FormKit | ngx-formly |
|---|---|---|---|---|---|
| Fundamentals | |||||
| One document for data and UI | Yes | JSON Forms: Partial. One document is enough for a plain form: with no uischema, `<JsonForms>` calls `Generate.uiSchema`. A conditional rule cannot be expressed by that generated layout, since rules live in the uischema; then it takes two documents. | RJSF: Yes. This registration form, including its Team only Seats requirement, is fully described by one JSON Schema object. No `uiSchema` is authored: labels come from `title`, widgets from `type`/`format`/`enum`, and the conditional requirement still validates. A `uiSchema` becomes a second document only once presentation needs changing. | FormKit: Yes. The registration form (name, email, plan, a conditional seats field, newsletter checkbox, with validation rules and a conditional) is one JSON serializable array passed to `FormKitSchema`. No second file, no separate validation schema, no separate layout description. | ngx-formly: Yes. One TypeScript array, `FormlyFieldConfig[]`, is the whole document: fields, labels, validation, layout and the conditional Seats field all live in it. There is no separate schema file and no separate template file to keep in sync with it. |
| Security | |||||
| Strict CSP by default, nothing to configure | Yes | JSON Forms: No. Nothing renders at all. AJV compiles the schema with eval before the first paint, so under script-src 'self' the root stays empty. This is the one library in the five with no way out that we could find. | RJSF: Partial. The fields render, but AJV compiles schemas with eval, so validation throws immediately under script-src 'self'. The form is inert past the first keystroke: it looks fine and does not validate. | FormKit: Yes. The free core is the only one of the five that is clean on both halves of the policy. It renders four inputs, runs its validation, and raises no script or style violation. | ngx-formly: Partial. The fields render, but string expressions are evaluated with eval, so expression properties throw and hide stops working under script-src 'self'. |
| Dynamic forms | |||||
| Named form states | Yes | JSON Forms: Partial. READONLY, their own effect for this, is a silent no op on Material's Group and Layout renderers: `enabled` reaches children, `readonly` never does (`layout.tsx`). DISABLE cascades, but a disabled field cannot take focus and does not submit, a different state. | RJSF: Partial. RJSF has no named state concept. The uiSchema is computed from formData.locked each render, setting `ui:readonly` on three fields and `ui:submitButtonOptions.submitText` by hand, about twenty lines. Locking a fourth field means adding a fourth line, not a single declaration. | FormKit: Partial. FormKit has no named state mechanism. A `readonly` prop on the wrapping group does not cascade, so `:readonly="locked"` has to repeat on each of the three fields. The submit label change comes from the same Vue ref, not from FormKit. | ngx-formly: Yes. formState is a first class part of FormlyFormOptions: a single toggle of options.formState.locked drives every field and the button label that reads it, instead of a duplicated ad hoc flag on each field. Each participating field still needs one short expression line naming the shared state, but the state itself is declared and flipped once. |
| Options from an API or driven by another field | Yes | JSON Forms: Partial. JSON Forms has no declarative way to point an enum at a URL. Their `dynamic-enum` tutorial solves it with a custom `Control` that fetches in `useEffect` and resets the dependent field; the fetch, loading state and reset are hand written. | RJSF: Partial. RJSF has no declarative way to point an enum at a URL. Reaching it takes two custom widgets fetching in useEffect, bridged through `registry.formContext` so the city widget sees the selected country, about fifty lines end to end. | FormKit: Partial. FormKit's `select` documents `options` as a plain array or object, never a function or a promise. An async function passed directly as `options` is never called, confirmed by running it: the dropdown stays on its placeholder. The working version fetches in `onMounted` and `watch`, then hands `options` a plain array. | ngx-formly: Yes. props.options accepts an Observable directly, so hooks.onInit can point it at a fetch() and, for the dependent field, at the parent field's own valueChanges piped through switchMap into a second fetch. Both requests are real HTTP calls the browser made, not a stub. |
| Computed values and text | Yes | JSON Forms: Partial. Their `middleware` docs show recomputing a dependent field on every `UPDATE_DATA` action. It stays reactive as either input changes, but the multiplication is hand written; there is no declarative expression for a derived value. | RJSF: Partial. RJSF has no derived field declaration. The summary property is an ordinary schema field marked `ui:readonly`, and the Form is controlled: the app's own onChange handler recomputes `summary` from `first` and `last` and feeds it back in as formData. It works and updates on every keystroke, but the derivation is application code, not something the schema describes. | FormKit: Yes. FormKit's schema has a declarative computed syntax: a `$:` expression string combined with `$get()` reaches another input's live value by its `id`. The summary line is one schema node, no Vue watcher, no computed(), no code outside the schema array. | ngx-formly: Yes. expressionProperties targeting a `model.*` path is a core feature: formly writes the computed value straight into the model and patches the field's own control, so a plain readonly input shows it. Nothing but the field config and the expression string was needed. |
| Validation | |||||
| Choosing when errors appear | Yes | JSON Forms: Partial. `validationMode` is a real prop with two states, show and hide; keystroke timing is the default with no extra code. Blur and submit are not modes JSON Forms ships; reaching them means toggling `validationMode` from focus, blur and submit handlers. | RJSF: Yes. The `liveValidate` prop on Form takes `false`, `"onChange"` or `"onBlur"` (RJSF 6 replaced the old boolean with this three way string, the boolean is deprecated). All three timings are one prop each, no custom code. | FormKit: Yes. FormKit's `validationVisibility` form config takes `dirty`, `blur` or `submit` and needs nothing else: no custom debounce, no manual blur handler, no submit interception. All three timings are one config line each on an otherwise identical field. | ngx-formly: Yes. modelOptions.updateOn is a first class field option, straight from Angular's reactive forms, that formly passes through untouched. All three fields start required and invalid; typing a character clears the error the moment the control's value syncs, which updateOn alone controls. No other code was needed. |
| Conditionally required fields that work | Yes | JSON Forms: Yes. Plain JSON Schema `if/then` makes Seats required only when Plan is Team, no uischema rule, no custom code. The label's required asterisk is static and does not follow `if/then`; only Ajv's validation message does. | RJSF: Yes. A schema `dependencies` block with `oneOf` adds the Seats property and its `required` entry only when Plan is Team. Ajv8 (the bundled validator) enforces it with no code outside the schema. The one cost is that Ajv8 also raises a generic "must match exactly one schema in oneOf" line for the object itself, which a reader has to explain away. | FormKit: Partial. FormKit has no rule like `required_if` and no documented recipe for conditional validation. `validation` is a plain reactive Vue prop, so a `computed()` feeding it turns the required rule and its message on the instant Plan becomes Team. | ngx-formly: Yes. ngx-formly ships no default copy for its own required check: without it, the error box renders empty and invisible to a user. One global validationMessages line fixes that for the whole app. Seats itself becomes required from a single props.required expression tied to model.plan, shown without a blur via validation.show:true. |
| Bringing your own validation library | Yes | JSON Forms: Partial. The schema carries no validation keywords, so Ajv has nothing to say; Zod is the only validator that runs. Its issues are mapped into the `ErrorObject` shape the documented `additionalErrors` prop expects, and that mapping is hand written. | RJSF: Partial. A `validator` implementing ValidatorType is mandatory on every Form, so Ajv8 cannot be removed. With no constraints in the schema, Ajv8 raises nothing and every message comes from a Zod schema inside the documented `customValidate` hook, pushed onto fields with `errors[key].addError()`. | FormKit: Yes. FormKit publishes an official @formkit/zod package. createZodPlugin(schema, onSubmit) hands back a plugin and a submit handler; none of the fields carry a FormKit `validation` string at all, Zod is the only validator running, and its per field messages land in FormKit's normal error slot on the matching input. | ngx-formly: Partial. No field carries a required or minLength prop. Every error comes from one Zod object schema, wired per field through validators.<name>.expression and .message. Zod's own messages appear, but each field needs its own hand written adapter, not a schema formly reads directly. |
| Layout | |||||
| Columns of different widths, spans, responsive wrap | Yes | JSON Forms: Partial. `HorizontalLayout` only ever splits children into equal `1/n` columns; `material-renderers`' own `layout.tsx` hardcodes `Grid size='grow'` for every child. A custom layout renderer, the extension point their `custom-layouts` tutorial documents, adds the fraction widths and the breakpoint with CSS grid. | RJSF: Yes. RJSF 6 ships a first class `LayoutGridField`. Assigning `ui:field: "LayoutGridField"` and a `ui:layoutGrid.ui:row` with two `ui:col` entries lays City and ZIP out on the MUI theme's Grid, and passing MUI's own responsive `size: { xs, sm }` props gets the 480px breakpoint by way of a custom MUI theme. It is a declarative uiSchema addition, no custom component. | FormKit: Partial. FormKit has no grid or column system; its own styling docs call layout plain CSS on top of `outerClass`. The two fields sit in a flex row with `outerClass`, and the two thirds and one third split and the 480px breakpoint are hand written CSS. Every pixel of the layout is ours, not FormKit's. | ngx-formly: Yes. fieldGroupClassName plus className are documented, first class field options mapped straight onto Bootstrap's row/col-* grid (col-8 and col-4 are exactly 2/3 and 1/3). Bootstrap has no 480px breakpoint of its own, so one small media query was added for that exact width; every other part is field config. |
| Tabs | Yes | JSON Forms: Yes. `Categorization`/`Category` is a first class uischema layout; the Material renderer set renders it as MUI Tabs by default. Data lives in one core state object regardless of which tab is mounted, so switching tabs never loses anything. | RJSF: Partial. RJSF has no tabs option. A custom `ui:ObjectFieldTemplate` pulls each field's rendered content into MUI Tabs, hiding the inactive panel with CSS instead of unmounting it, so its formData survives the switch. Tab grouping is hard coded in the template, not read from the schema. | FormKit: Yes. `@formkit/addons` ships `createMultiStepPlugin`, a free, first party plugin, not FormKit Pro, with `multi-step` and `step` input types and a `tabStyle` option that renders it as tabs. Step data lives on the same form node the whole time, so nothing is lost switching tabs. | ngx-formly: Partial. ngx-formly ships no tabs type. Their own docs build one as a custom FieldType wrapping `mat-tab-group`; this reuses the same pattern on Bootstrap `nav-tabs`, hand written rather than read from the schema. Each tab stays a live fieldGroup while hidden, so switching tabs never loses data. |
| Accordion sections | Yes | JSON Forms: Partial. Material renderers only ship an accordion for repeated array rows, `ExpandPanelRenderer`, not for fixed sections of a form. A custom layout renderer built on MUI's own `Accordion`, per their `custom-layouts` tutorial, gives two sections with the second collapsed by default. | RJSF: Partial. Same extension point as tabs: a custom `ui:ObjectFieldTemplate` hands each field's content to a pair of MUI Accordions. The second section starts collapsed by local state, and MUI keeps its children mounted, just hidden, so a typed value survives closing it. Section grouping is hard coded in the template. | FormKit: Partial. FormKit has no accordion component. Its schema and Vue templates happily render plain markup alongside FormKit inputs, so the two sections are ordinary native <details>/<summary> elements with FormKit fields inside them; the open/close behaviour is the browser's, not FormKit's. This is a workaround the library makes easy, not a feature it ships. | ngx-formly: Partial. formly's own docs show FieldWrapper used for a plain bordered panel around a fieldGroup, always open, with no toggle. This extends that same wrapper with an open flag read from props.expanded and a click handler, hand written since their docs do not provide one. |
| Frameworks | |||||
| React, Angular, Vue and Lit | Yes | JSON Forms: Partial. JSON Forms publishes three bindings, React, Angular and Vue, all at `3.8.0`. Their own renderer sets documentation marks the Vuetify set as preview with known bugs and shows uneven support across sets, not parity. | RJSF: No. RJSF publishes one framework binding: React. What looks like a wide list of packages (antd, mui, `chakra-ui`, mantine, primereact, `react-bootstrap`, `semantic-ui`, shadcn, `fluentui-rc`, plus the bootstrap 3 default in core) are all React component themes, each declaring `react` as a peer dependency, not separate framework integrations. There is no Vue, Angular, Svelte or vanilla binding published under the @rjsf scope. | FormKit: Partial. FormKit publishes @formkit/vue and @formkit/react as separate, official, same version packages sharing one core. Both install and build cleanly and render the same three fields, name, email, plan, with the same validation strings against the same @formkit/core. | ngx-formly: No. Every package under the @ngx-formly npm scope targets Angular: bootstrap, material, ionic, primeng, kendo, `ng-zorro-antd` and nativescript all build on @angular/core. No React, Vue or Svelte binding is published; `vue-formly`, `svelte-formly` and `formly-reactjs` on npm are unrelated third party packages. |
| Web components and plain JavaScript | Yes | JSON Forms: No. Their integration docs list React, Angular and Vue only. `@jsonforms/vanilla-renderers`, despite its name, still lists `react` and `@jsonforms/react` as peer dependencies; it swaps Material for plain HTML widgets without dropping the React requirement. No package registers a custom element. | RJSF: No. RJSF has no framework free mount path. Every package is a React component: using one on a page needs React, ReactDOM and a `createRoot().render()` call. There is no custom element, no UMD build for a bare `<script>` tag, and no vanilla entry point in the docs. | FormKit: No. Loading @formkit/vue's no build IIFE bundle on a page with no Vue global throws `ReferenceError: Vue is not defined`; its last line closes over a bare global `Vue`. No `customElements.define` call exists in @formkit/vue, @formkit/core or @formkit/inputs. FormKit cannot run without the Vue framework present. | ngx-formly: No. customElements.get() returns undefined for `formly-form`, `formly-field` and `formly-group` after a real form renders. @ngx-formly/core exports only Angular components and services: no custom element build, no mount() call, no framework free path documented. |
| Server side rendering | Yes | JSON Forms: Partial. JSON Forms ships no server entry and its documentation names no host. `@jsonforms/react` is a plain React tree, so React's own `renderToStaticMarkup` renders it in Node and the result is real HTML. The server rendering is React's, and the entry is one you write. | RJSF: Partial. RJSF ships no server entry and its documentation names no host. Its Forms are ordinary React components, so `react-dom/server` renders one in plain Node and the HTML is real. This is React's own documented path, not something RJSF provides. | FormKit: Partial. `@formkit/vue` ships no server entry. It is a standard Vue plugin, so Vue's own `createSSRApp` and `@vue/server-renderer` render it in Node and the HTML is real. FormKit does publish one official meta framework module, `@formkit/nuxt`, covering a single host. | ngx-formly: Partial. `@ngx-formly/core` ships no server entry and its documentation names no host. `ng add @angular/ssr` plus a server route prerenders the form to static HTML with the model's value baked in, which is Angular's machinery, not formly's. |
| Components | |||||
| Ships its own complete component set | Yes | JSON Forms: Partial. `@jsonforms/material-renderers`' peer dependencies name `@mui/material`, `@mui/icons-material` and `@mui/x-date-pickers`, binding it to a third party kit. `@jsonforms/vanilla-renderers` depends only on `lodash` and draws its own plain `<select>`/`<input>` elements with no UI kit underneath. | RJSF: Partial. @rjsf/core ships its own widgets, plain HTML with bootstrap classes, no UI kit dependency beyond `markdown-to-jsx`. Every polished theme (mui, antd, `chakra-ui`, mantine, primereact, `react-bootstrap`, `semantic-ui`, `fluentui-rc`) declares that framework's own kit as a peer dependency instead. The components a real app renders are MUI's or antd's, not RJSF's. | FormKit: Yes. Every FormKit input in the free core is FormKit's own code. @formkit/inputs, which defines every input type, depends only on @formkit/core and @formkit/utils; @formkit/themes depends only on @formkit/core. None of the packages a normal FormKit install pulls in depend on a third party form or UI kit. | ngx-formly: Partial. @ngx-formly/core ships no widgets and depends only on tslib. @ngx-formly/bootstrap writes its own Angular templates styled with Bootstrap's CSS, no bootstrap JS; the material, primeng, kendo and `ng-zorro-antd` packages instead wrap those kits' own components. |
| File upload | Yes | JSON Forms: Partial. `material-renderers` ships no file control; nothing in their source matches `File`. A custom `Control` with a hidden native file input uses the same extension point as any custom control, writing the chosen file's name into form state through `handleChange`. | RJSF: Yes. A `string` property with `format: "data-url"` renders RJSF's built in FileWidget with no uiSchema needed. Picking a file encodes it as base64 into formData as a data URL that carries the original filename (`data:<type>;name=<name>;base64,<data>`), so the name lands in form state without any code beyond the schema. | FormKit: Yes. FormKit's `file` input is a normal free core input. Its value is an array of `{ name, file }` objects, one per selected file, so reading the file's name back out of the form's reactive state is a single property read on the bound value. | ngx-formly: Partial. Angular reactive forms cannot bind `<input type=file>` out of the box, a known Angular limitation, not formly's. Their own docs supply the fix as a small ControlValueAccessor plus a `file` FieldType. Once registered, `type: 'file'` is declarative like any other field and the chosen FileList lands straight in the model. |
| Repeater: a list of rows the user adds and removes | Yes | JSON Forms: Yes. A schema of type array with object items is enough; `MaterialArrayControlRenderer` ships add, remove and reorder, with each row's two fields addressed by its own path. No custom renderer was written. | RJSF: Yes. A schema property of `type: "array"` with an `items` object schema is enough: RJSF renders the add button, and a remove button (plus reordering buttons) on every row, and keeps each row's fields addressed by index in formData. No uiSchema or custom code was needed for the base behaviour. | FormKit: Partial. FormKit's own `repeater` input, with drag handles and add or remove built in, lives in FormKit Pro, paid and key gated, not run here. The free core's documented alternative is a `list` input with a `group` per row by numeric index and hand written add and remove buttons. Values stay attached to the right row across add and remove. | ngx-formly: Partial. `FieldArrayType` is exported by @ngx-formly/core specifically for this, and it does the hard part: keeping one FormArray entry, and one model slice, per row. The render template (the row markup, the add/remove buttons) is still the developer's own, following their own documented recipe almost line for line. |
| Date, time and date time range pickers | Yes | JSON Forms: Partial. Their docs cover `date`, `time` and `date-time` formats; a true range picker ships in MUI X Pro, not the free `@mui/x-date-pickers`. A custom `Control` composes two `DatePicker` cells bound to one object property, and both dates land in form state. | RJSF: Partial. RJSF ships no range widget, only single date inputs. The schema models the range as an object with `start` and `end`, and a custom `ui:field` renders both as one bordered control wired into the form's own state. The one control look and the object shape are both hand written. | FormKit: Partial. FormKit's date picker lives in FormKit Pro, paid and key gated, not run here. Pro's own docs describe no range mode for it either. The free core's `date` input is a single HTML date field, so the measured result is two grouped date inputs, not one range control. | ngx-formly: Partial. No ngx-formly package ships a date range control, and their docs give no recipe for one either. Built as a custom FieldType: two native date inputs behind one field, value {start, end}, entirely hand written. |
Yes Partial, requires a workaround Not supported
Theming
On top of the components out of the box, theme them to your liking, or go headless.
Drag to compare. Same form, two themes.
Or integrate your own components.
What sets us apart.
All four frameworks
The same document and the same widgets render in React, Angular, Vue and Lit.
Plain JS and web components
Mount a form with a custom element and no framework at all.
Server side rendering
Documented for all four frameworks, with starters for Next, Nuxt, Analog and Astro.
Your own template for every option
Render each option in a select, radio group or list with your own markup: a flag beside a country, an avatar beside a user.
i18n with your own library
Every label, placeholder and validator message can be a translation key resolved by the i18n library you already use.
DX TypeScript layer
Build a form with autocompleted, type checked gui.* factories that emit plain data.
MCP server
Validates a definition, type checks gui.* code and generates a form from a JSON Schema or an OpenAPI operation.
Agent skill and CLI
Installs with one command and runs the same checks in CI.
Ready to start building?
One JSON document, one engine, every framework. Install it, or hold it against the library you use today.
golem noun /ˈɡəʊləm/ a figure made of clay that comes to life.
GolemUI and its clay theme take their name and character from this figure.