Schema to Form Fields

How a backend JSON Schema becomes real form controls - the support matrix, field types, fieldHint, reference pickers and validation.

This page is the frontend side of the conversion - what Thunder UI does with the JSON Schema it receives. To author that schema, see Controlling the UI From the Backend and the Schema Metadata Reference.

src/core/crud/metadata.ts exports fieldsFromModuleMetadata(metadata, opts). It picks the schema that matches the intent and converts it with JSONSchemaToFields.toFields:

IntentSchema resolution
insertcrud.insertSchema ?? crud.schema
updatecrud.updateSchema ?? crud.insertSchema ?? crud.schema
outputcrud.schema

The function is total - it always returns TField[] and never rejects. A module that exposes create/update without a schema yields an empty array, and the form shows an empty state.

The worked schema on the Schema Metadata page exercises most of this in one place.

JSON Schema Support Matrix

ConstructResult
object with propertiesnested field group, arbitrarily deep
array of objectsrepeatable group (orders.0.lines.0.sku)
array of scalarstag input (multi)
array of arrays of scalarsper-row tag input
requiredpropagates, including inside array items
enumdropdown (strings or numbers)
format: date, date-time, email, uri, e164date / date / email / url / phone
$ref / $defsresolved, including recursive refs
allOfmerged into the host schema
oneOf / anyOfbranches merged into one object; a const discriminator becomes an enum
type: ["string","null"]resolves to the non-null type
defaultbecomes the field's initial value
constsubmitted, rendered hidden
additionalPropertiesJSON textarea
prefixItemssibling fields name.0, name.1

Default rule: a property is optional unless its parent lists it in required.

Unsupported constructs degrade to a text input and emit a one-time console.warn. Set JSONSchemaToFields.silent = true to suppress those warnings.

Field Type to Control

FieldControl
text, maxLength absent or above 100textarea
text, maxLength 100 or belowsingle-line input
numbernumber input
booleanswitch
datedate input
email / urlinput with the matching type
phoneinternational phone input (lazy-loaded)
hiddennot rendered; still registered if it has const
enum presentdropdown, or multiselect when multi
ref presentsearchable, server-paginated picker
array of objectsrepeatable group with add/remove
objectnested field group
any multi scalartag input

fieldHint

A schema property may carry fieldHint to pick a specific control instead of the default for its type. On the backend you set it with .meta({ fieldHint }):

fieldHintApplies toControl
avatarurl, singleavatar uploader
uploadurlfile-table uploader (fileType, fileSize, maxItems honoured)
markdowntextmarkdown editor (lazy-loaded)
jsontextJSON textarea; blocks invalid JSON
autocompleteenumtype-ahead instead of a dropdown
amountnumberamount-styled number input
datetime-localdatedate + time input
filtersobjectMongoDB filter builder; requires filterSchema

The avatar and upload hints wire straight into ImageKit.

Reference Fields

A field with ref renders a searchable, server-paginated picker that queries the referenced module on demand - 25 per page, debounced search, and every request abortable. This is what Thunder's foreign-reference fields produce.

KeyMeaning
refmodule name this field references
refLabelproperty or properties to display (string or string[])
refValueproperty to submit (default _id)
refFiltersfilters applied when querying the referenced module

TField

The shape the renderers consume. Any extra key on a JSON Schema property is carried through onto the field - that is how the hints above work, and how a .meta() key you invent yourself reaches your own override component.

KeyMeaning
typea field type from the table above, or "array" / "object"
namedotted path used as the form field name
label, descriptionpassed through t()
placeholder, exampleplaceholder text
optionaltrue unless the parent requires it
defaultValuefrom the schema's default
multiscalar array, rendered as a tag input
enumstring[] or an array of { label, value }
minLength, maxLength, minimum, maximum, minItems, maxItems, patternvalidation
fieldHintsee above
constsingle legal value; submitted, not editable
groupconsecutive fields sharing a group render in one FieldGroup
groupClassName, groupStyle, className, stylestyling passthrough
requirementKeyrender only when the watched field's value equals this field's name
fileType, fileSizeupload constraints
queryValue, ignoreQueryValueprefill from a URL query parameter, or opt out
fieldschild fields for array and object

className and groupClassName can arrive from a backend schema at runtime, where Tailwind's scanner cannot see them. src/index.css ships an @source inline(...) block that force-generates the grid and gap utilities for exactly this reason - don't remove it.

Validation

buildRules(field, t) derives the react-hook-form rules from the schema:

  • required
  • pattern
  • minLength / maxLength for text
  • min / max for numbers
  • minItems / maxItems for arrays

All messages go through t(), so they are translatable.

Server-side messages surface automatically: an axios interceptor registered in initThunder() raises one toast per entry in error.response.data.messages. Aborted requests never toast.


On this page