Quick Start
A complete set of ShadCN-based renderers for JSONForms — schema-driven forms without writing repetitive form code.
JSONForms generates forms from a JSON Schema and a UI Schema. Instead of hand-coding every input, you describe your data model and layout declaratively, and the renderer handles the rest.
This registry provides a drop-in renderer set built entirely on ShadCN components — no extra design system, no style conflicts. The code lands directly in your project so you own and customize it.
Installation
Install everything at once:
npx shadcn@latest add https://mksingh.dev/r/jsonforms-renderers.jsonThis installs all renderers, layouts, and cells under components/forms/renderers/, the utility helpers under components/forms/utils.ts, and pulls in the required ShadCN components automatically.
Install individually
The utils helper is also available standalone:
npx shadcn@latest add https://mksingh.dev/r/jsonforms-utils.jsonEvery renderer is also available as a standalone registry item:
npx shadcn@latest add https://mksingh.dev/r/jsonforms-boolean-control.jsonnpx shadcn@latest add https://mksingh.dev/r/jsonforms-array-layout.jsonnpx shadcn@latest add https://mksingh.dev/r/jsonforms-enum-cell.jsonQuick start
'use client';
import { useState } from 'react';
import { JsonForms } from '@jsonforms/react';
import { jsonFormsCells, jsonFormsRenderers } from '~/components/forms/renderers';
const schema = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
age: { type: 'integer', minimum: 0 },
},
required: ['name'],
};
const uischema = {
type: 'VerticalLayout',
elements: [
{ type: 'Control', scope: '#/properties/name' },
{ type: 'Control', scope: '#/properties/age' },
],
};
export default function MyForm() {
const [data, setData] = useState({});
return (
<JsonForms
schema={schema}
uischema={uischema}
data={data}
renderers={jsonFormsRenderers}
cells={jsonFormsCells}
onChange={({ data }) => setData(data)}
/>
);
}How it works
JSONForms uses a tester/renderer system. When rendering a field, it runs all registered testers against the current schema node and picks the renderer with the highest score.
This means:
- Renderers compose without configuration — the right one is selected automatically.
- You can override any renderer by registering a new one with a higher rank.
- Adding a renderer is additive — it doesn't break existing ones.
Validation & errors
Validation runs on every change via AJV. Errors are hidden by default. Control when they appear using config.shouldValidate:
| Mode | Behaviour |
|---|---|
onBlur | Show errors after each field loses focus |
onSubmit | Show all errors at once (set this on form submission) |
<JsonForms
// ...
config={{ shouldValidate: 'onBlur' }}
/>For submit-gated validation, toggle the mode on submission:
const [shouldValidate, setShouldValidate] = useState<ShouldValidate | undefined>(undefined);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setShouldValidate('onSubmit');
};
<JsonForms
// ...
config={{ shouldValidate }}
/>Custom error messages
Pass a translateError function via the i18n prop to replace AJV's default messages with human-readable ones. The function receives the AJV error object and should return a string, or an empty string to fall back to the default.
Both translateError and ShouldValidate ship in components/forms/utils.ts:
import { translateError, type ShouldValidate } from '~/components/forms/utils';
<JsonForms
// ...
i18n={{ translateError }}
config={{ shouldValidate }}
/>Add errorMessage to your JSON Schema properties to provide per-keyword messages:
{
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 2,
"errorMessage": {
"minLength": "Name must be at least 2 characters."
}
}
},
"required": ["name"]
}Each renderer displays only the first error line (errors.split('\n')[0]), so a single, focused message is shown even when multiple validations fail simultaneously.
Full example with submit / reset
'use client';
import { useState } from 'react';
import { JsonForms } from '@jsonforms/react';
import { Button } from '~/components/ui/button';
import { jsonFormsCells, jsonFormsRenderers } from '~/components/forms/renderers';
import { translateError, type ShouldValidate } from '~/components/forms/utils';
const schema = { /* ... */ };
const uischema = { /* ... */ };
export default function MyForm() {
const [data, setData] = useState({});
const [shouldValidate, setShouldValidate] = useState<ShouldValidate | undefined>(undefined);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setShouldValidate('onSubmit');
};
const handleReset = () => {
setData({});
setShouldValidate(undefined);
};
return (
<form onSubmit={handleSubmit}>
<JsonForms
schema={schema}
uischema={uischema}
data={data}
renderers={jsonFormsRenderers}
cells={jsonFormsCells}
i18n={{ translateError }}
config={{ shouldValidate }}
onChange={({ data }) => setData(data)}
/>
<div className="flex gap-2 mt-4 justify-end">
<Button variant="secondary" type="button" onClick={handleReset}>Reset</Button>
<Button type="submit">Submit</Button>
</div>
</form>
);
}