Skip to content

feat: add SchemaForm component for JSON Schema-driven plugin config UI#3316

Open
Geetanjali-147 wants to merge 1 commit intoapache:masterfrom
Geetanjali-147:feat/schema-form
Open

feat: add SchemaForm component for JSON Schema-driven plugin config UI#3316
Geetanjali-147 wants to merge 1 commit intoapache:masterfrom
Geetanjali-147:feat/schema-form

Conversation

@Geetanjali-147
Copy link

Summary

Implements a reusable SchemaForm component that automatically renders
form 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

  • Add SchemaForm component — renders forms from JSON Schema
  • Add schemaParser.ts — parses JSON Schema into field definitions
  • Add widgetMapper.ts — maps field types to existing UI components
  • Add unit tests (33 tests passing)
  • Add README documentation with developer guide

Supported Schema Types

  • string → TextInput
  • number / integer → NumberInput
  • boolean → Switch
  • enum → Select dropdown
  • array → TagsInput
  • object → JsonInput / nested fieldset
  • oneOf → Type selector + dynamic fields
  • format: password → PasswordInput

Supported Constraints

  • required fields
  • minimum / maximum for numbers
  • minLength / maxLength for strings
  • pattern regex validation
  • default values

Validation

Two-layer validation pipeline:

  1. react-hook-form — inline field validation on change
  2. AJV — full JSON Schema validation on submit

Tests

33 unit tests covering:

  • All basic field type parsing
  • Required field detection
  • Constraint parsing
  • Widget mapping
  • Validation rules
  • Real APISIX plugin schema (limit-req)

How to Test

import { SchemaForm } from '@/components/schema-form/SchemaForm';

<SchemaForm
  schema={{
    type: 'object',
    properties: {
      rate: { type: 'number', minimum: 0 },
      burst: { type: 'number', minimum: 0 },
      key: {
        type: 'string',
        enum: ['remote_addr', 'server_addr', 'consumer_name']
      }
    },
    required: ['rate', 'burst', 'key']
  }}
  onSubmit={(values) => console.log(values)}
/>

Why submit this pull request?

  • New feature provided
  • Did you explain what problem does this PR solve?
  • Have you added corresponding test cases?
  • Have you modified the corresponding document?
  • Is this PR backward compatible?

What changes will this PR take into?

This PR adds a reusable SchemaForm component that automatically
renders form fields from a JSON Schema definition.

New Files

  • src/components/schema-form/SchemaForm.tsx — Main component that
    renders forms automatically from any JSON Schema
  • src/components/schema-form/schemaParser.ts — Parses JSON Schema
    into a flat list of typed field definitions
  • src/components/schema-form/widgetMapper.ts — Maps field types to
    existing Mantine UI components
  • src/components/schema-form/__tests__/ — 33 unit tests
  • src/components/schema-form/README.md — Developer guide

Supported Types

string, number, integer, boolean, enum, array, object, oneOf

Validation

Two-layer validation using react-hook-form + AJV

fix/resolve #2986

Checklist:

  • Did you explain what problem does this PR solve? Yes
  • Have you added corresponding test cases? Yes — 33 unit tests
  • Have you modified the corresponding document? Yes — README.md added
  • Is this PR backward compatible? Yes — new files only, no existing code modified
Screenshot 2026-03-04 194036

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 SchemaForm React 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.

Comment on lines +143 to +145
const rules = getValidationRules(field);
const error = errors[field.name]?.message as string | undefined;
const value = watch(field.name as never);
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +74 to +77
const onFormSubmit = (values: Record<string, unknown>) => {
const validate = ajv.compile(schema);
const valid = validate(values);

Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +67
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;
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
/**
* SchemaForm.tsx
* Main component that renders a form automatically
* from a JSON Schema definition.
*
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
/**
* schemaParser.test.ts
* Unit tests for the schema parser utility
*/

Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +40
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 });

Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +188 to +192
<Switch
label={field.label}
description={field.description}
checked={!!value}
onChange={(e) => setValue(field.name as never, e.currentTarget.checked)}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
/**
* widgetMapper.ts
* Maps a ParsedField type to the correct
* existing form component in the project.
*/
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
/**
* widgetMapper.test.ts
* Unit tests for the widget mapper utility
*/

Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +52
field.maxLength === undefined ||
field.maxLength > 100
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
field.maxLength === undefined ||
field.maxLength > 100
field.maxLength !== undefined &&
field.maxLength <= 100

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support JSONSchema to Form UI

2 participants