render option forms #628

Closed
opened 2025-12-03 15:24:54 +01:00 by kiara · 1 comment
Owner

As a Fediversity maintainer,
I want to offer schemas to render relevant options,
so that we may present available configuration options to operators.

implementation notes

library comparison

Library comparison

RJSF JSONForms JSFE
Framework React only React / Vue 3 / Angular Web Component (Lit)
Bundle (island cost) React + theme (100s of kB) Framework + renderers (~200 kB+ Material) ~5 kB Lit + component
Maturity 15.8k★, v6.6.1 (May 2026), heavy production use 2.7k★, v3.7.0 (Nov 2025), commercial backing 178★, "not for production, major rewrite"
Default styling fit (Bulma) needs theme/restyle Material-flavored, needs restyle Shoelace/Material/Carbon adapters

JSON Schema coverage

Feature RJSF JSONForms JSFE
Draft 7 ⚠️ unpinned
Draft 2019-09 / 2020-12 via AJV swap via AJV swap
$ref inline (external needs preprocessing) 🚧 WIP
oneOf / anyOf (flat dropdown UX) combinator renderers 🚧 WIP
allOf merged 🚧 WIP
if / then / else (use dependencies) (use UISchema Rules) 🚧 WIP
dependencies first-class ⚠️ partial 🚧 WIP
additionalProperties add-key UI ⚠️ limited ⚠️ basic
patternProperties
Recursive schemas 🚧 WIP

UISchema / customization

RJSF JSONForms JSFE
Formal UISchema spec ad-hoc uiSchema object documented spec basic ui:widget hints
Layout primitives via templates Horizontal/Vertical/Group/Categorization minimal
Conditional show/hide via schema dependencies/oneOf Rules engine (SHOW/HIDE/ENABLE/DISABLE + JSON-pointer condition)
Custom widgets pluggable widgets/fields/templates tester-based renderer dispatch Lit templates
Custom validators AJV8 default AJV ⚠️ limited
Field ordering / grouping ui:order declarative via UISchema scopes minimal
Read-only / disable per field via uiSchema via Rules partial

Fit for Fediversity specifics

RJSF JSONForms JSFE
Handles deeply nested submodules + attrsOf
Discriminated unions (types.either/oneOf) ⚠️ flat-dropdown UX combinator renderers 🚧
#213 schema-diff annotations ⚠️ via custom templates tester-based custom renderers
#214 dual-form side-by-side + read-only + rename-alignment ⚠️ awkward declarative via two UISchemas + Rules

RJSF vs JSONForms — feature deep-dive

1. additionalProperties (Nix attrsOf)

  • RJSF: first-class. additionalProperties: <schema> renders the value with its full subschema (so attrsOf submodule gives you a nested form per entry). UX is a list of key + value rows with an Add button; keys are editable text inputs, remove control per row, toggleable via ui:options.expandable. No propertyNames-style key validation.
  • JSONForms: not "limited" gracefully — inconsistent across renderer packs. Open issue #2492 requests additionalProperties for the React-Material pack, with an abandoned PR #2494. The Vue/Vuetify pack has working support (PRs #2409, #2481 merged) but with known bugs around mixed properties+additionalProperties, null/object gaps, and data-insertion-into-wrong-field.

Net difference: RJSF gives a working map editor out of the box. JSONForms-React needs a custom renderer (finite cost — a few hundred lines — but a real one).

2. dependencies and the enable-gate pattern

  • RJSF: dependencies is first-class in two flavors.
    • Property dependencies: "dependencies": { "x": ["y"] } → if x is set, y becomes required (no show/hide, only validation).
    • Schema dependencies: "dependencies": { "x": { "properties": {...} } } → dependent properties appear/disappear based on the trigger field's presence.
    • For the enable gate idiom, the standard pattern is oneOf inside a schema dependency keyed on enable, branching on enum: [true] vs enum: [false]. Verbose but works.
  • JSONForms: JSON Schema dependencies is not the recommended mechanism — use UISchema Rules:
    { "type": "Control", "scope": "#/properties/port",
      "rule": { "effect": "SHOW",
                "condition": { "scope": "#/properties/enable", "schema": { "const": true } } } }
    
    Effects: SHOW / HIDE / ENABLE / DISABLE. Rules can attach to any UI element, so ENABLE/DISABLE on a Group cascades to all its children — the natural fit for enable-gating.

Net difference: RJSF expresses the gate in the schema (so a Nix-side codegen can emit it directly). JSONForms expresses it in UISchema rules — cleaner and cascading, but you must auto-generate UISchema alongside the data schema (post-processing generateDefaultUISchema(schema) to inject rules wherever a sibling is named enable).

3. oneOf (Nix either/oneOf)

  • RJSF: single <select> above the variant subform. Labels from each subschema's title (or "Option N"). On data load, getMatchingOption picks the first subschema whose validation passes — works with const discriminator fields, less well when subschemas overlap. No first-class discriminator keyword. Customizable via ui:fieldReplacesAnyOrOneOf / OneOfField / AnyOfField overrides.
  • JSONForms: default combinator renderer is also a dropdown (Material) or tabs (Vanilla, configurable via options.detail). AJV-based variant detection. No first-class discriminator either, but tester-based dispatch lets you install one custom renderer matching s.oneOf && s.oneOf.every(v => v.properties?.kind?.const) and apply it across the whole schema.

Net difference: small. Both default to dropdowns. JSONForms' tester dispatch makes installing a "tagged-union" widget once-and-everywhere cleaner.

4. Schema diff annotations (#213)

  • RJSF: pass the diff object via formContext (free-form prop threaded into every field/widget/template). A custom FieldTemplate reads props.formContext.diff[props.id] and wraps props.children with badges. Caveat: props.id is RJSF's dotted-underscores path (root_services_foo_port), not a JSON pointer — you convert.
  • JSONForms: custom renderers receive path (dotted) and uischema.scope (which is a JSON pointer, e.g. #/properties/services/properties/foo/properties/port). A high-priority ControlWrapper (rankWith(1000, () => true)) wraps every control and indexes diff[scope] directly. Diff object delivered via React context (no formContext analogue, but equivalent).

Net difference: both can do it. JSONForms' scope is the natural key into a diff object; RJSF's id needs translation. Cleaner code on the JSONForms side.

5. Dual side-by-side forms with field alignment (#214)

  • RJSF: <Form readonly> / <Form disabled> for whole-form, ui:readonly / ui:disabled per-field. Two <Form> instances side-by-side works — but row-level vertical alignment is not built in. Each form derives layout from its own schema; renamed fields (different ids) don't align automatically. You'd write a shared FieldTemplate with consistent row heights and orchestrate alignment via CSS-grid yourself.
  • JSONForms: effect: "DISABLE" on a VerticalLayout disables everything underneath. Two UISchemas over different data schemas can share layout structure because UISchema is positional and controls reference scopes — old.scope = "#/properties/dbUrl" and new.scope = "#/properties/databaseUrl" can sit at the same row of each VerticalLayout. Prior art: Eclipse Theia preferences editor, EclipseSource EMF.cloud model-migration UIs — driving coordinated views off evolving schemas is the use case JSONForms was designed for.

Net difference: this is the clearest win. RJSF can be coerced into a migration view but the layout-coordination burden is on you. JSONForms' UISchema-as-layout was designed for exactly this scenario.

Summary

Point Leader Margin
additionalProperties / attrsOf RJSF significant — JSONForms-React needs a custom map renderer
enable-gate (dependencies / rules) JSONForms moderate — cleaner, cascades over Groups, but needs UISchema codegen
oneOf UX JSONForms small — tester dispatch is more elegant for discriminator patterns
#213 diff annotations JSONForms small — scope-as-JSON-pointer is a cleaner diff key than RJSF's id
#214 dual-form migration JSONForms large — designed-for use case vs DIY layout coordination

The trade now reads as: RJSF wins decisively on attrsOf today (which is pervasive in NixOS — services, users, vhosts). JSONForms wins on every long-term ticket (#213, #214) and on the enable idiom (also pervasive). The JSONForms map-renderer is a finite, one-off engineering cost; the RJSF migration-view coordination is open-ended.

**As** a Fediversity maintainer, **I want** to offer schemas to render relevant options, **so that** we may present available configuration options to operators. ### implementation notes - [ ] support UI tweaks, see e.g.: - `UISchema` (#987) - [thymis](https://github.com/Thymis-io/thymis#screenshot--demo) - nix - [annotated options](https://github.com/NixOS/nixpkgs/pull/358906) - [arbitrary metadata](https://github.com/NixOS/nixpkgs/pull/341199) - do these offer sufficient flexibility vs say django forms' [output styles](https://docs.djangoproject.com/en/5.1/ref/forms/api/#output-styles) (paragraph, list, table, granular)? - [ ] for `panel` - django - [x] `pydantic` - [x] [`django-jsonform`](https://github.com/bhch/django-jsonform) (#895, uses [`react-jsonschema-form`](https://github.com/rjsf-team/react-jsonschema-form)) - [ ] htmx: ? - [ ] web component: [`json-schema-form-element`](https://github.com/json-schema-form-element/jsfe) - agnostic: - [ ] [JSONForms](https://github.com/eclipsesource/jsonforms) (covers draft-07) - frame-work-specific: - react: [`react-jsonschema-form`](https://github.com/rjsf-team/react-jsonschema-form) (covers draft-07) <details> <summary> library comparison </summary> ## Library comparison | | RJSF | JSONForms | JSFE | | ------------------------------- | ----------------------------------------------- | -------------------------------------------- | --------------------------------------------- | | **Framework** | React only | React / Vue 3 / Angular | Web Component (Lit) | | **Bundle (island cost)** | React + theme (100s of kB) | Framework + renderers (~200 kB+ Material) | ~5 kB Lit + component | | **Maturity** | 15.8k★, v6.6.1 (May 2026), heavy production use | 2.7k★, v3.7.0 (Nov 2025), commercial backing | 178★, **"not for production, major rewrite"** | | **Default styling fit (Bulma)** | needs theme/restyle | Material-flavored, needs restyle | Shoelace/Material/Carbon adapters | ### JSON Schema coverage | Feature | RJSF | JSONForms | JSFE | | ----------------------- | ---------------------------------------- | ----------------------- | ----------- | | Draft 7 | ✅ | ✅ | ⚠️ unpinned | | Draft 2019-09 / 2020-12 | ✅ via AJV swap | ✅ via AJV swap | ❌ | | `$ref` | ✅ inline (external needs preprocessing) | ✅ | 🚧 WIP | | `oneOf` / `anyOf` | ✅ (flat dropdown UX) | ✅ combinator renderers | 🚧 WIP | | `allOf` | ✅ merged | ✅ | 🚧 WIP | | `if` / `then` / `else` | ❌ (use `dependencies`) | ❌ (use UISchema Rules) | 🚧 WIP | | `dependencies` | ✅ first-class | ⚠️ partial | 🚧 WIP | | `additionalProperties` | ✅ add-key UI | ⚠️ limited | ⚠️ basic | | `patternProperties` | ❌ | ❌ | ❌ | | Recursive schemas | ✅ | ✅ | 🚧 WIP | ### UISchema / customization | | RJSF | JSONForms | JSFE | | --------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------- | -------------------------- | | **Formal UISchema spec** | ❌ ad-hoc `uiSchema` object | ✅ **documented spec** | ❌ basic `ui:widget` hints | | **Layout primitives** | via templates | `Horizontal`/`Vertical`/`Group`/`Categorization` | minimal | | **Conditional show/hide** | via schema `dependencies`/`oneOf` | ✅ **Rules engine** (`SHOW`/`HIDE`/`ENABLE`/`DISABLE` + JSON-pointer condition) | ❌ | | **Custom widgets** | ✅ pluggable widgets/fields/templates | ✅ tester-based renderer dispatch | ✅ Lit templates | | **Custom validators** | ✅ AJV8 default | ✅ AJV | ⚠️ limited | | **Field ordering / grouping** | `ui:order` | declarative via UISchema scopes | minimal | | **Read-only / disable per field** | via uiSchema | via Rules | partial | ### Fit for Fediversity specifics | | RJSF | JSONForms | JSFE | | ---------------------------------------------------------- | ----------------------- | ---------------------------------------- | -------------- | | Handles deeply nested submodules + `attrsOf` | ✅ | ✅ | ❌ | | Discriminated unions (`types.either`/`oneOf`) | ⚠️ flat-dropdown UX | ✅ combinator renderers | 🚧 | | #213 schema-diff annotations | ⚠️ via custom templates | ✅ tester-based custom renderers | ❌ | | #214 dual-form side-by-side + read-only + rename-alignment | ⚠️ awkward | ✅ declarative via two UISchemas + Rules | ❌ | # RJSF vs JSONForms — feature deep-dive ## 1. `additionalProperties` (Nix `attrsOf`) - **RJSF**: first-class. `additionalProperties: <schema>` renders the value with its full subschema (so `attrsOf submodule` gives you a nested form per entry). UX is a list of `key + value` rows with an **Add** button; keys are editable text inputs, remove control per row, toggleable via `ui:options.expandable`. No `propertyNames`-style key validation. - **JSONForms**: not "limited" gracefully — **inconsistent across renderer packs**. Open issue [#2492](https://github.com/eclipsesource/jsonforms/issues/2492) requests `additionalProperties` for the React-Material pack, with an abandoned PR [#2494](https://github.com/eclipsesource/jsonforms/pull/2494). The **Vue/Vuetify** pack has working support (PRs #2409, #2481 merged) but with known bugs around mixed `properties`+`additionalProperties`, null/object gaps, and data-insertion-into-wrong-field. **Net difference**: RJSF gives a working map editor out of the box. JSONForms-React needs a custom renderer (finite cost — a few hundred lines — but a real one). ## 2. `dependencies` and the `enable`-gate pattern - **RJSF**: `dependencies` is first-class in two flavors. - **Property dependencies**: `"dependencies": { "x": ["y"] }` → if `x` is set, `y` becomes required (no show/hide, only validation). - **Schema dependencies**: `"dependencies": { "x": { "properties": {...} } }` → dependent properties **appear/disappear** based on the trigger field's presence. - For the `enable` gate idiom, the standard pattern is `oneOf` inside a schema dependency keyed on `enable`, branching on `enum: [true]` vs `enum: [false]`. Verbose but works. - **JSONForms**: JSON Schema `dependencies` is *not* the recommended mechanism — use UISchema **Rules**: ```json { "type": "Control", "scope": "#/properties/port", "rule": { "effect": "SHOW", "condition": { "scope": "#/properties/enable", "schema": { "const": true } } } } ``` Effects: `SHOW` / `HIDE` / `ENABLE` / `DISABLE`. Rules can attach to **any** UI element, so `ENABLE`/`DISABLE` on a `Group` cascades to all its children — the natural fit for `enable`-gating. **Net difference**: RJSF expresses the gate in the schema (so a Nix-side codegen can emit it directly). JSONForms expresses it in UISchema rules — cleaner and cascading, but you must auto-generate UISchema alongside the data schema (post-processing `generateDefaultUISchema(schema)` to inject rules wherever a sibling is named `enable`). ## 3. `oneOf` (Nix `either`/`oneOf`) - **RJSF**: single `<select>` above the variant subform. Labels from each subschema's `title` (or "Option N"). On data load, `getMatchingOption` picks the first subschema whose validation passes — works with `const` discriminator fields, less well when subschemas overlap. No first-class `discriminator` keyword. Customizable via `ui:fieldReplacesAnyOrOneOf` / `OneOfField` / `AnyOfField` overrides. - **JSONForms**: default combinator renderer is also a dropdown (Material) or tabs (Vanilla, configurable via `options.detail`). AJV-based variant detection. No first-class discriminator either, but tester-based dispatch lets you install **one** custom renderer matching `s.oneOf && s.oneOf.every(v => v.properties?.kind?.const)` and apply it across the whole schema. **Net difference**: small. Both default to dropdowns. JSONForms' tester dispatch makes installing a "tagged-union" widget once-and-everywhere cleaner. ## 4. Schema diff annotations (#213) - **RJSF**: pass the diff object via `formContext` (free-form prop threaded into every field/widget/template). A custom `FieldTemplate` reads `props.formContext.diff[props.id]` and wraps `props.children` with badges. Caveat: `props.id` is RJSF's dotted-underscores path (`root_services_foo_port`), not a JSON pointer — you convert. - **JSONForms**: custom renderers receive `path` (dotted) and `uischema.scope` (which **is** a JSON pointer, e.g. `#/properties/services/properties/foo/properties/port`). A high-priority `ControlWrapper` (`rankWith(1000, () => true)`) wraps every control and indexes `diff[scope]` directly. Diff object delivered via React context (no `formContext` analogue, but equivalent). **Net difference**: both can do it. JSONForms' `scope` is the natural key into a diff object; RJSF's `id` needs translation. Cleaner code on the JSONForms side. ## 5. Dual side-by-side forms with field alignment (#214) - **RJSF**: `<Form readonly>` / `<Form disabled>` for whole-form, `ui:readonly` / `ui:disabled` per-field. Two `<Form>` instances side-by-side works — but **row-level vertical alignment is not built in**. Each form derives layout from its own schema; renamed fields (different `id`s) don't align automatically. You'd write a shared `FieldTemplate` with consistent row heights and orchestrate alignment via CSS-grid yourself. - **JSONForms**: `effect: "DISABLE"` on a `VerticalLayout` disables everything underneath. Two UISchemas over different data schemas can **share layout structure** because UISchema is positional and controls reference scopes — `old.scope = "#/properties/dbUrl"` and `new.scope = "#/properties/databaseUrl"` can sit at the same row of each `VerticalLayout`. Prior art: Eclipse Theia preferences editor, EclipseSource `EMF.cloud` model-migration UIs — driving coordinated views off evolving schemas is the use case JSONForms was *designed* for. **Net difference**: this is the clearest win. RJSF can be coerced into a migration view but the layout-coordination burden is on you. JSONForms' UISchema-as-layout was designed for exactly this scenario. ## Summary | Point | Leader | Margin | | -------------------------------------- | ------------- | ---------------------------------------------------------------------- | | `additionalProperties` / `attrsOf` | **RJSF** | significant — JSONForms-React needs a custom map renderer | | `enable`-gate (`dependencies` / rules) | **JSONForms** | moderate — cleaner, cascades over Groups, but needs UISchema codegen | | `oneOf` UX | **JSONForms** | small — tester dispatch is more elegant for discriminator patterns | | #213 diff annotations | **JSONForms** | small — `scope`-as-JSON-pointer is a cleaner diff key than RJSF's `id` | | #214 dual-form migration | **JSONForms** | large — designed-for use case vs DIY layout coordination | The trade now reads as: **RJSF wins decisively on `attrsOf` today** (which is pervasive in NixOS — services, users, vhosts). **JSONForms wins on every long-term ticket** (#213, #214) and on the `enable` idiom (also pervasive). The JSONForms map-renderer is a finite, one-off engineering cost; the RJSF migration-view coordination is open-ended. </details>
Author
Owner

Closed in #1017.

Closed in #1017.
kiara closed this issue 2026-06-08 00:02:50 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
fediversity/fediversity#628
No description provided.