Navigation

Schema Forms

The sheet's settings, its column create and edit dialogs and the dashboard's visual editor render their forms from a Zod schema. zodToJsonSchema turns the form schema into JSON Schema, and UiSchemaForm renders that: JSON Forms' core lays it out and keeps every nested value in step, the library's own components draw every node, and the Zod schema the form came from is what validates it.

How a form is drawn

flowchart TD
  Z[The form's Zod schema] --> J[zodToJsonSchema]
  J --> F[JSON Forms' core, its own validation off]
  F --> D{UiSchemaFormDispatch, by rank}
  D -->|a fixed value: a variant's discriminant| H[Nothing drawn]
  D -->|an enum, a titled enum, a field naming a context key| S[UiSelect, one value or several]
  D -->|a union of objects| O[The variant choice, over the chosen variant's fields]
  D -->|a string or a number| T[UiTextField, one line or several]
  D -->|a boolean| C[UiCheckbox]
  D -->|a layout or a nested object| L[The fields stacked, framed under a label when nested]
  D -->|an array| A[A card per item, added, moved and removed]
  L --> D
  A --> D
  S --> W[The value written at the field's path]
  O --> W
  T --> W
  C --> W
  W --> Q[The Zod schema validates the whole value]
  Q --> I[Each issue shown on the field at its path, once the reader has changed it]
  • JSON Schema is the input. JSON Forms generates the layout from the schema where no UI schema is given, which is every form the app has, and zodToJsonSchema's inline snapshot tests pin the schema each form receives.
  • The library draws every node. UiSchemaForm holds JSON Forms' core state and provides it to the bindings, and UiSchemaFormDispatch draws each node with the renderer whose tester ranks it highest in SchemaFormRenderers: layouts, nested objects and arrays as well as text, numbers, booleans and choices. A new kind of node is one entry in that list. The state is shallow: JSON Forms' reducers return a new core for every change, so the replaced property is the only change to track, and the renderer components are never proxied.
  • Layout meta is typed data. A field that wants several lines, or a choice among items only the dialog knows, says so in its layout meta — a flag, or a key of the dialog's context interface — never an expression string evaluated at runtime.
  • The Zod schema validates. JSON Forms runs with its validation off. The form's Zod schema checks the whole value on every change, as the dialog's error icon does, and each issue reaches the field at its path through the form's config, shown once the reader has changed that field. A rule that reads live state, a column name unique among the sheet's, is a refinement the dialog's composable builds.
  • A variant is its discriminant. A union's variants each fix their discriminant to a literal, which the form hides and the variant choice sets. Switching keeps the fields the variants share and anything the schema does not describe, such as a column's id, and drops the old variant's own — and a shared field the two variants pick from different lists of the dialog's context, since the new picker never offered the old choice.

The engine is adopted, the Vue shell is ours

A schema form engine is most of the work: generating a layout from the schema, ranking each node's renderer, detecting which variant of a union the data is in, and applying every change to a nested value immutably. Writing that for a handful of dialogs would put an engine in the repository that nothing else maintains, so JSON Forms' core and the setup-only useJsonForms* bindings are adopted. Its Vue components are not: the JsonForms root keeps its state in data(), DispatchRenderer picks a renderer in a computed, and the vanilla renderers are written the same way, while the app compiles the Options API out. Mounted there, the root rendered off an undefined store and aborted the patch around it. So the root, the dispatch and the layout, object and array renderers are the library's own <script setup> components, and every Vitest run loads Vue with the Options API off (getVueTestConfiguration), so a component that needs it fails its test rather than the app.

CandidateWhy it is or is not the one
JSON Forms' core, chosenRenders the JSON Schema the app already generates, framework-free, with every renderer registered by rank. Maintained by EclipseSource, with the largest user base of the Vue schema engines
JSON Forms' Vue components and vanilla renderers, rejectedWritten in the Options API, which the app compiles out; turning vue.optionsApi back on would be a whole-app cost paid for one dependency
vjsf, replacedBound to Vuetify as a peer dependency, so Vuetify could not leave while it stayed, and its fields could not wear the library's look
shadcn-vue's AutoFormWalks the Zod schema itself, but it is copied into the repository rather than installed — a renderer of our own under another name — and it brings Reka UI and vee-validate beside Vuetify 0
FormKitA whole form framework whose inputs, validation and schema format would replace the library's fields and the app's JSON Schema

AJV is JSON Forms'

JSON Forms' core imports AJV and builds an instance when a form mounts, even with its validation off, to match a union's variants. So AJV and its formats stay, as JSON Forms' own dependencies rather than the app's. Their CommonJS needs nothing of ours: rolldown wraps it in its own interop helper in the production build, and the dev pre-bundle names them through their importer (configuration/vite.ts). What went with vjsf is everything the app wrote on top: its validation keywords, its error-message and translation packages, the layout engine's debug, and the pre-bundle list vjsf published.

Key files

FileRole
apps/web/app/components/Ui/SchemaForm/Index.vueJSON Forms' core state, provided to the bindings, with the Zod issues in its config
apps/web/app/components/Ui/SchemaForm/Dispatch.vueDraws a node with the renderer that ranks it highest
apps/web/app/components/Ui/SchemaForm/Layout.vueA layout's elements stacked, or framed under a label when nested
apps/web/app/components/Ui/SchemaForm/Object.vueA nested object as its own fields
apps/web/app/components/Ui/SchemaForm/Array.vueA list's items, added, moved and removed
apps/web/app/services/ui/schemaForm/SchemaFormRenderers.tsWhich renderer draws which node, by rank
apps/web/app/services/ui/schemaForm/getSchemaFormVariantValue.tsThe value a union switched to another variant keeps
apps/web/app/composables/ui/useSchemaFormControl.tsWhat every field reads: its layout meta and its issue
apps/web/app/services/jsonSchema/zodToJsonSchema.tsThe form's JSON Schema, with its layout meta
apps/web/shared/models/schemaForm/SchemaFormLayout.tsThe layout meta a field may carry
apps/web/app/services/ui/schemaForm/getSchemaFormLayout.tsThe layout meta read off a node, which JSON Forms' types leave out
apps/web/app/composables/resource/sheet/useColumnForm.tsThe column dialogs' context and their refined form schema
packages/configuration/vitest/registerVueEsmBundler.jsEvery test's Vue with the Options API off, as the app ships it

Sources

Details

Command palette

Keyboard shortcuts