feat: add SchemaForm component for JSON Schema-driven plugin config UI#3316
feat: add SchemaForm component for JSON Schema-driven plugin config UI#3316Geetanjali-147 wants to merge 1 commit intoapache:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new JSON Schema–driven form renderer intended to auto-generate APISIX plugin configuration UIs from their shipped JSON Schemas, reducing manual form implementation work.
Changes:
- Introduces
SchemaFormReact component to render inputs and validate with react-hook-form + AJV. - Adds schema parsing (
schemaParser.ts) and widget selection/validation mapping (widgetMapper.ts). - Adds unit tests for schema parsing and widget mapping; adds Jest DOM setup.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| src/setupTests.ts | Adds Jest DOM matchers for unit tests. |
| src/routeTree.gen.ts | Updates generated route type mappings (notably index-route fullPath trailing slashes). |
| src/components/schema-form/SchemaForm.tsx | New schema-driven form component with AJV submit validation and dynamic field rendering (incl. nested objects / oneOf). |
| src/components/schema-form/schemaParser.ts | Parses JSON Schema into ParsedField definitions for rendering. |
| src/components/schema-form/widgetMapper.ts | Maps parsed fields to widget types and derives RHF validation rules. |
| src/components/schema-form/tests/schemaParser.test.ts | Unit tests for schema parsing behavior. |
| src/components/schema-form/tests/widgetMapper.test.ts | Unit tests for widget mapping and validation rule generation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const rules = getValidationRules(field); | ||
| const error = errors[field.name]?.message as string | undefined; | ||
| const value = watch(field.name as never); |
There was a problem hiding this comment.
errors[field.name] won’t work for nested field names like parent.child (RHF nests errors as objects). This breaks error display for nested objects and oneOf subfields. Use useController/Controller (preferred in this codebase) or a safe getter (e.g. get(errors, field.name)) to resolve nested errors.
| const onFormSubmit = (values: Record<string, unknown>) => { | ||
| const validate = ajv.compile(schema); | ||
| const valid = validate(values); | ||
|
|
There was a problem hiding this comment.
AJV compilation happens on every submit (ajv.compile(schema)), which is relatively expensive and can throw if the schema uses unsupported dialect/keywords. Consider memoizing the compiled validator (e.g. useMemo) and wrapping compilation/validation in try/catch so a bad schema can be surfaced as a form error instead of crashing the component.
| export interface JSONSchema { | ||
| type?: string | string[]; | ||
| properties?: Record<string, JSONSchema>; | ||
| required?: string[]; | ||
| enum?: unknown[]; | ||
| default?: unknown; | ||
| minimum?: number; | ||
| maximum?: number; | ||
| minLength?: number; | ||
| maxLength?: number; | ||
| pattern?: string; | ||
| description?: string; | ||
| title?: string; | ||
| oneOf?: JSONSchema[]; | ||
| anyOf?: JSONSchema[]; | ||
| items?: JSONSchema; | ||
| dependencies?: Record<string, JSONSchema>; | ||
| if?: JSONSchema; | ||
| then?: JSONSchema; | ||
| else?: JSONSchema; | ||
| } |
There was a problem hiding this comment.
PR description mentions supporting format: password, but the schema types here do not include format, and parsing/mapping never checks it. Add format?: string to JSONSchema (and carry it into ParsedField) so getWidget can reliably map format: 'password' to PasswordInput instead of inferring from the field name.
| /** | ||
| * SchemaForm.tsx | ||
| * Main component that renders a form automatically | ||
| * from a JSON Schema definition. | ||
| * |
There was a problem hiding this comment.
This file is missing the required ASF license header block. ESLint enforces headers/header-format for src/**/*.{ts,tsx} (see eslint.config.ts), so this will fail lint unless the standard header is added at the top.
| /** | ||
| * schemaParser.test.ts | ||
| * Unit tests for the schema parser utility | ||
| */ | ||
|
|
There was a problem hiding this comment.
This test file is missing the required ASF license header block. ESLint enforces headers/header-format for src/**/*.{ts,tsx}, so this will fail lint unless the standard header is added at the top.
| import Ajv from 'ajv'; | ||
|
|
||
| import { parseSchema, type JSONSchema, type ParsedField } from './schemaParser'; | ||
| import { getWidget, getValidationRules } from './widgetMapper'; | ||
|
|
||
| // ── AJV setup ──────────────────────────────────────────────────────────────── | ||
| const ajv = new Ajv({ allErrors: true }); | ||
|
|
There was a problem hiding this comment.
ajv is imported here, but it is not listed in package.json dependencies/devDependencies. This will fail installs/builds in a clean environment unless ajv is added as a direct dependency (and any required ajv plugins, e.g. formats, if needed).
| <Switch | ||
| label={field.label} | ||
| description={field.description} | ||
| checked={!!value} | ||
| onChange={(e) => setValue(field.name as never, e.currentTarget.checked)} |
There was a problem hiding this comment.
These controlled widgets (e.g. Switch here, and also Select/NumberInput/TagsInput/JsonInput below) are driven via watch/setValue but are never registered with react-hook-form. That means getValidationRules(field) won’t run for them and required/min/max validation won’t happen inline. Consider using Controller/useController (or the existing FormItem* components under src/components/form/*) so rules and errors are wired consistently.
| /** | ||
| * widgetMapper.ts | ||
| * Maps a ParsedField type to the correct | ||
| * existing form component in the project. | ||
| */ |
There was a problem hiding this comment.
This file is missing the required ASF license header block. ESLint enforces headers/header-format for src/**/*.{ts,tsx} (see eslint.config.ts), so this will fail lint unless the standard header is added at the top.
| /** | ||
| * widgetMapper.test.ts | ||
| * Unit tests for the widget mapper utility | ||
| */ | ||
|
|
There was a problem hiding this comment.
This test file is missing the required ASF license header block. ESLint enforces headers/header-format for src/**/*.{ts,tsx}, so this will fail lint unless the standard header is added at the top.
| field.maxLength === undefined || | ||
| field.maxLength > 100 |
There was a problem hiding this comment.
The “long text gets textarea” branch appears inverted: when maxLength is undefined or > 100 it currently returns TextInput. That will render long/unknown-length strings as a single-line input. Either swap the returned widgets or adjust the comment/threshold logic so it matches the intended behavior.
| field.maxLength === undefined || | |
| field.maxLength > 100 | |
| field.maxLength !== undefined && | |
| field.maxLength <= 100 |
Summary
Implements a reusable
SchemaFormcomponent that automatically rendersform fields from a JSON Schema definition, eliminating manual UI work
for APISIX plugin configuration forms.
Motivation
Closes #2986
APISIX plugins ship JSON Schema definitions for their configuration.
This PR enables the dashboard to render plugin config forms directly
from those schemas, improving developer experience significantly.
Changes
SchemaFormcomponent — renders forms from JSON SchemaschemaParser.ts— parses JSON Schema into field definitionswidgetMapper.ts— maps field types to existing UI componentsSupported Schema Types
string→ TextInputnumber/integer→ NumberInputboolean→ Switchenum→ Select dropdownarray→ TagsInputobject→ JsonInput / nested fieldsetoneOf→ Type selector + dynamic fieldsformat: password→ PasswordInputSupported Constraints
requiredfieldsminimum/maximumfor numbersminLength/maxLengthfor stringspatternregex validationdefaultvaluesValidation
Two-layer validation pipeline:
react-hook-form— inline field validation on changeAJV— full JSON Schema validation on submitTests
33 unit tests covering:
How to Test
Why submit this pull request?
What changes will this PR take into?
This PR adds a reusable
SchemaFormcomponent that automaticallyrenders form fields from a JSON Schema definition.
New Files
src/components/schema-form/SchemaForm.tsx— Main component thatrenders forms automatically from any JSON Schema
src/components/schema-form/schemaParser.ts— Parses JSON Schemainto a flat list of typed field definitions
src/components/schema-form/widgetMapper.ts— Maps field types toexisting Mantine UI components
src/components/schema-form/__tests__/— 33 unit testssrc/components/schema-form/README.md— Developer guideSupported Types
string, number, integer, boolean, enum, array, object, oneOf
Validation
Two-layer validation using react-hook-form + AJV
fix/resolve #2986
Checklist: