Skip to content

GSOC 26 Proof of Concept: add SchemaForm component for JSON Schema to Form UI#3274

Open
DSingh0304 wants to merge 28 commits intoapache:masterfrom
DSingh0304:feat/json-schema-form-clean
Open

GSOC 26 Proof of Concept: add SchemaForm component for JSON Schema to Form UI#3274
DSingh0304 wants to merge 28 commits intoapache:masterfrom
DSingh0304:feat/json-schema-form-clean

Conversation

@DSingh0304
Copy link
Contributor

@DSingh0304 DSingh0304 commented Jan 5, 2026

JSON Schema Form UI: anyOf, if/then/else, patternProperties & Form State Cleanup

Closes #3315
Closes #2986

Summary

This PR is a proof of concept for the GSoC 2026 "Dynamic Plugin Configuration UI" project. It demonstrates a production-ready, recursive form engine that handles complex JSON Schema Draft 7 constructs.

Beyond adding new keywords, this update focuses on Correctness and Data Integrity, ensuring that the form state sent to the APISIX backend is always minimal and strictly adheres to the active schema branch.

What's Changed (Recent Fixes & Polish)

1. Automatic Form State Cleanup (High Priority)

Implemented unregister() logic in OneOfFields, AnyOfFields, and IfThenElseFields.

  • The Problem: Previously, switching from a redis backend to a local backend would leave "ghost" values (like redis_host) in the final submit payload, potentially causing backend validation failures.
  • The Fix: Stale fields are now instantly removed from the form state the moment a branch is deselected. This ensures "Submit Payload Consistency" as requested by maintainers.

2. patternProperties Support (Dynamic Key-Value Maps)

Many APISIX plugins (like proxy-rewrite or response-rewrite) require dynamic HTTP header configuration.

  • Added the PatternPropertiesField component to handle schemas where keys are regex-based strings.
  • Included the serializePatternProperties utility to transform internal form state into the flat object structure required by the APISIX Admin API.

3. Smart Label Formatting

Refined the formatLabel utility to handle technical acronyms. Fields like oauth_client_id or api_key_uri now render professionally as "OAuth Client ID" and "API Key URI" instead of camel-cased defaults.

4. Clean Build (Zero Warnings)

Resolved all linting errors and warnings across the codebase.

  • Refactored SchemaForm.tsx to resolve Fast Refresh issues.
  • Fixed pre-existing Playwright E2E warnings using non-null assertions.
  • Validated with pnpm lint and --max-warnings=0.

Core Engine Features

1. anyOf conditional field groups (AnyOfFields)

Automatically detects discriminator const values and renders the active branch's fields reactively. Essential for the limit-count plugin's backend selection.

2. if / then / else conditional rendering (IfThenElseFields)

Uses AJV to evaluate nested schemas against live form data. Powers toggles like "Enable Burst Mode" in the limit-conn plugin.

3. encrypt_fields masking

Automatically transforms TextInput into masked PasswordInput for sensitive keys like jwt-auth secrets.


Tests - 50 Passing (0 Failures, 0 Warnings)

This PR includes the project's first comprehensive unit and integration test suite for SchemaForm:

  • validation.test.ts — AJAX schema resolution and logic.
  • SchemaField.test.tsx — Widget mapping and labels.
  • SchemaForm.test.tsx — Integration testing for conditional rendering and Form State Cleanup.
Test Files  3 passed (3)
Tests       50 passed (50)
Duration    ~2s

Interactive Demo

Accessible at http://localhost:5173/ui/schema_form_demo.
Features a "Live Form Data" monitor that allows maintainers to verify that fields are purged from the JSON payload in real-time as branches are toggled.

Video Proof

Screen recording demonstrating the implemented features live on the development server.

Screencast.from.2026-03-24.18-14-13.mp4

What the recording shows:

  • Cleanup Logic: Toggling "Enable Burst Mode" and seeing fields being added/removed from the live JSON block instantly.
  • PatternProperties: Adding and removing custom HTTP headers in the proxy-rewrite demo.
  • Green Build: Running pnpm test and pnpm lint to show a perfectly clean environment.

@DSingh0304
Copy link
Contributor Author

@Baoyuantop Is this approach aligned with the project's direction?

@SkyeYoung
Copy link
Member

This may require more time to discuss.

@DSingh0304
Copy link
Contributor Author

This may require more time to discuss.

Yes sir sure ...

@SkyeYoung SkyeYoung self-assigned this Jan 12, 2026
- Add SchemaForm component that renders forms from JSON Schema
- Support for oneOf (conditional field groups)
- Support for dependencies (conditional fields based on other values)
- Reuses existing FormItem components (TextInput, Select, Switch, NumberInput)
- Add demo page at /schema_form_demo for testing
Instead of globally disabling emptyObjectsCleaner which broke timeout
validation, this fix uses a marker-based approach that only preserves
empty objects in the plugins field. Other empty objects (like timeout)
are still cleaned as expected.

- Add producePreservePlugins to mark empty plugin configs before cleaning
- Add produceRemovePreserveMarkers to restore them after cleaning
- This prevents both bugs: plugins being dropped AND invalid timeout errors
- Add ArrayField component using useFieldArray for object arrays
- Add string array support using TagsInput
- Create validation.ts with AJV integration
- Add createSchemaResolver for React Hook Form
- Map AJV errors to user-friendly messages
- Test with real proxy-rewrite plugin schema
- Add tabs to demo page for schema comparison

Addresses feedback from Issue apache#2986
Install the test stack needed for SchemaForm unit/component tests:
- vitest ^4.0.18 (test runner, replaces jest)
- @vitest/coverage-v8 (coverage via v8)
- @testing-library/react, @testing-library/user-event (component testing)
- @testing-library/jest-dom (custom matchers e.g. toBeInTheDocument)
- jsdom (browser-like DOM for vitest)
- vitest.config.ts: jsdom env, include only src/**/*.test.*, @ alias,
  ~icons/* alias (stubs unplugin-icons), v8 coverage config
- src/test/setup.ts: window.matchMedia + ResizeObserver stubs for Mantine,
  @testing-library/jest-dom matchers registered globally
- src/test/utils.tsx: renderWithForm/FormWrapper helpers wrapping
  MantineProvider + react-hook-form FormProvider
- src/test/__mocks__/Icon.tsx: null stub for ~icons/material-symbols/* used
  by ArrayField (unplugin-icons not available in jsdom)
- .gitignore: exclude vitest coverage output directory
…SchemaField

SchemaField now accepts an isEncrypted boolean prop. When true it renders
FormItemPasswordInput (masked input with reveal toggle) instead of the default
FormItemTextInput, supporting the APISIX-specific encrypt_fields extension.

SchemaForm.renderProperties() reads schema.encrypt_fields and passes the flag
down per-field automatically — no schema changes required by plugin authors.

Also removes unused _required param from ArrayField (pre-existing lint error).
Adds two new sub-components to SchemaForm:

AnyOfFields
  Handles JSON Schema anyOf the same way oneOf is handled — detects a
  discriminator field via const-keyed branches, watches its value with
  useWatch, and renders the matching branch's extra properties. In APISIX
  plugin schemas anyOf and oneOf share the same discriminator convention.

IfThenElseFields
  Handles JSON Schema Draft 7 if/then/else keywords. The if sub-schema is
  compiled and evaluated against live form data using the AJV instance from
  validation.ts on every render cycle. When it matches, the then schema's
  properties are rendered; otherwise the else schema's properties are shown.
  Uses useWatch({ control }) (all values, no name) so defaultValues are
  available immediately on the first render before fields register.

SchemaForm.renderProperties() is extended to read the APISIX-specific
encrypt_fields extension and pass isEncrypted to each SchemaField.

validation.ts: fix pre-existing quote-style lint error (singlequote rule).
validation.test.ts — 14 pure unit tests (no DOM)
  Tests validateWithSchema for required/type/minLength/maximum/pattern/
  enum/minItems/nested paths, and createSchemaResolver happy+error paths.

SchemaField.test.tsx — 11 component tests
  Verifies every schema type maps to the correct Mantine widget:
  string→TextInput, integer/number→NumberInput (inputmode=decimal),
  boolean→Switch, isEncrypted→PasswordInput, enum→Select (listbox),
  array-of-strings→TagsInput, array-of-objects→ArrayField (Add Item btn),
  nested object→Fieldset, and auto-formatted labels from snake_case names.

SchemaForm.test.tsx — 18 integration tests
  Covers all conditional rendering patterns with real form state:
  top-level properties, no-properties empty schema, encrypt_fields,
  enum/nested objects, oneOf branch selection, anyOf branch selection,
  dependencies with oneOf, if/then (condition matches),
  if/else (condition fails), no-else schema, basePath prefixing, and
  oneOf field isolation (two tests asserting mutual exclusion of branches).

Testing notes:
  - Mantine v8 Select renders aria-haspopup=listbox, not role=combobox
  - Mantine v8 NumberInput renders inputmode=decimal, not role=spinbutton
  - Required field labels include an asterisk span — queries use /^Label/
    regex to match despite the trailing asterisk in the accessible name
  - Floating-ui dropdown interaction not testable in jsdom — interactive
    oneOf switching is covered by the Playwright e2e suite instead
schema_form_demo.tsx
  Two new tab panels added to the existing demo route (/schema_form_demo):

  anyOf tab — limit-count plugin shape
    Demonstrates AnyOfFields: selecting 'redis' shows Redis connection
    fields; selecting 'local' shows the local policy rate-limit fields.
    Mirrors the real limit-count plugin's Redis vs local backend split.

  if/then/else tab — fault-injection plugin shape
    Demonstrates IfThenElseFields: toggling 'Enable Abort' (boolean)
    shows/hides the abort HTTP code and body fields in real time using
    the AJV-evaluated if/const:true condition.

docs/en/schema-form.md
  Developer guide covering:
  - Supported JSON Schema keywords and their widget mappings
  - How to add a new widget (step-by-step)
  - SchemaForm component API (schema, basePath props)
  - SchemaField prop table (name, schema, control, required, isEncrypted)
  - AJV validation integration and createSchemaResolver usage
  - Testing patterns with renderWithForm and the jsdom environment
  - Architecture notes on conditional rendering strategies
@DSingh0304 DSingh0304 changed the title feat: add SchemaForm component for JSON Schema to Form UI GSOC 26 Proof of Concept: add SchemaForm component for JSON Schema to Form UI Mar 3, 2026
Auto-generated by TanStack Router when schema_form_demo.tsx was added.
Registers /schema_form_demo in FileRoutesByFullPath, FileRoutesByTo, and FileRoutesById.
@DSingh0304
Copy link
Contributor Author

DSingh0304 commented Mar 3, 2026

Hi @Baoyuantop @SkyeYoung I've submitted this PoC PR for this issue as part of my GSoC 2026 application journey.

The branch adds anyOf, if/then/else, and encrypt_fields support to the existing SchemaForm, with 43 passing tests and an interactive demo at /schema_form_demo. A developer guide is included at schema-form.md.

Happy to discuss any implementation decisions or adjust based on feedback. Thank you!

…clusiveMinimum/Maximum

- Replace fault-injection demo schema with limit-conn burst configuration,
  aligning the if/then/else tab with the plugin listed in the proposal table
  (APISIX-39 section 6: if/then/else → limit-conn, response-rewrite, ...)
- demo now also exercises exclusiveMinimum (default_conn_delay must be > 0)
- Wire schema.exclusiveMinimum / schema.exclusiveMaximum to NumberInput min/max
  in SchemaField.tsx so the widget UI hint reflects exclusive boundaries;
  AJV still enforces strict exclusion on submit
@suryaparua-official
Copy link

Hi @DSingh0304

I tested the SchemaForm demo locally from this PR branch.

Steps to reproduce:

  1. Navigate to /ui/schema_form_demo
  2. Select auth_type = oauth
  3. Fill the OAuth fields
  4. Change auth_type to api_key

Expected behavior:
When switching to api_key, OAuth-related fields should be removed from the form state.

Actual behavior:
The OAuth fields remain in the form data even after switching to api_key.

Example form data after switching:

{
"auth_type": "api_key",
"oauth_client_id": "...",
"oauth_client_secret": "..."
}

It seems the previous oneOf branch values remain in the react-hook-form state.

Screenshot attached for reference.
Screenshot 2026-03-05 173751
Screenshot 2026-03-05 173830
Screenshot 2026-03-05 173858
Screenshot 2026-03-05 173935
Screenshot 2026-03-05 174003
Screenshot 2026-03-05 174021

Let me know if you would like me to explore a potential fix.

@suryaparua-official
Copy link

I also tested enabling shouldUnregister: true in useForm during local testing, and it removed the stale oneOf fields when switching variants.

It seems this behavior may be related to React Hook Form keeping unmounted fields in form state by default.

@DSingh0304
Copy link
Contributor Author

Hi @suryaparua-official I have been testing the form myself and I came across these issue. I will resolve them soon, I have been busy. Thank you for pointing the issues out. They will be resolved soon.

@Jaswanth-arjun
Copy link

Jaswanth-arjun commented Mar 24, 2026

Hi @DSingh0304,

I tested the SchemaForm demo and was able to reproduce the issue where switching between schema branches (oneOf / anyOf / if-then-else) retains stale field values in the form state.

I worked on a fix for this by implementing branch-aware cleanup inside SchemaForm. Instead of relying only on shouldUnregister, the solution explicitly unregisters fields that are no longer part of the active schema branch.

Solution

  • Tracks active schema branch paths
  • Automatically calls unregister on fields from previous branches
  • Works for:
    • oneOf
    • anyOf
    • if/then/else
    • dependencies

Testing

  • Verified via /schema_form_demo
  • Switching branches correctly removes stale values
  • No regression in existing behavior

I’d be happy to open a PR or contribute to the existing one if this approach aligns with the project direction. Please let me know your thoughts.

@DSingh0304
Copy link
Contributor Author

Hi @DSingh0304,

I tested the SchemaForm demo and was able to reproduce the issue where switching between schema branches (oneOf / anyOf / if-then-else) retains stale field values in the form state.

I worked on a fix for this by implementing branch-aware cleanup inside SchemaForm. Instead of relying only on shouldUnregister, the solution explicitly unregisters fields that are no longer part of the active schema branch.

Solution

* Tracks active schema branch paths

* Automatically calls `unregister` on fields from previous branches

* Works for:
  
  * oneOf
  * anyOf
  * if/then/else
  * dependencies

Testing

* Verified via `/schema_form_demo`

* Switching branches correctly removes stale values

* No regression in existing behavior

I’d be happy to open a PR or contribute to the existing one if this approach aligns with the project direction. Please let me know your thoughts.

Hey @Jaswanth-arjun I have fixed that issue in these recent commits, thank you for reporting the issue and i have added the test for it as well. I will update the PR description, proposal and screenshot and video proof as well.

@Jaswanth-arjun
Copy link

Hi @DSingh0304,
I tested the SchemaForm demo and was able to reproduce the issue where switching between schema branches (oneOf / anyOf / if-then-else) retains stale field values in the form state.
I worked on a fix for this by implementing branch-aware cleanup inside SchemaForm. Instead of relying only on shouldUnregister, the solution explicitly unregisters fields that are no longer part of the active schema branch.

Solution

* Tracks active schema branch paths

* Automatically calls `unregister` on fields from previous branches

* Works for:
  
  * oneOf
  * anyOf
  * if/then/else
  * dependencies

Testing

* Verified via `/schema_form_demo`

* Switching branches correctly removes stale values

* No regression in existing behavior

I’d be happy to open a PR or contribute to the existing one if this approach aligns with the project direction. Please let me know your thoughts.

Hey @Jaswanth-arjun I have fixed that issue in these recent commits, thank you for reporting the issue and i have added the test for it as well. I will update the PR description, proposal and screenshot and video proof as well.

Thanks for the update! @DSingh0304

I also worked on this issue and implemented a similar branch-cleanup approach with additional testing locally.

I’d be happy to help further improve this PR. Possible areas I can contribute:

  • Additional edge-case testing (nested schemas, repeated switching)
  • Performance improvements for large schemas
  • UI/UX improvements for generated forms
  • Documentation improvements for SchemaForm behavior

Please let me know if I can help with anything here.

@DSingh0304
Copy link
Contributor Author

Hi @DSingh0304,
I tested the SchemaForm demo and was able to reproduce the issue where switching between schema branches (oneOf / anyOf / if-then-else) retains stale field values in the form state.
I worked on a fix for this by implementing branch-aware cleanup inside SchemaForm. Instead of relying only on shouldUnregister, the solution explicitly unregisters fields that are no longer part of the active schema branch.

Solution

* Tracks active schema branch paths

* Automatically calls `unregister` on fields from previous branches

* Works for:
  
  * oneOf
  * anyOf
  * if/then/else
  * dependencies

Testing

* Verified via `/schema_form_demo`

* Switching branches correctly removes stale values

* No regression in existing behavior

I’d be happy to open a PR or contribute to the existing one if this approach aligns with the project direction. Please let me know your thoughts.

Hey @Jaswanth-arjun I have fixed that issue in these recent commits, thank you for reporting the issue and i have added the test for it as well. I will update the PR description, proposal and screenshot and video proof as well.

Thanks for the update! @DSingh0304

I also worked on this issue and implemented a similar branch-cleanup approach with additional testing locally.

I’d be happy to help further improve this PR. Possible areas I can contribute:

* Additional edge-case testing (nested schemas, repeated switching)

* Performance improvements for large schemas

* UI/UX improvements for generated forms

* Documentation improvements for SchemaForm behavior

Please let me know if I can help with anything here.

Yes sure I will let you know if I need any help.

@DSingh0304
Copy link
Contributor Author

DSingh0304 commented Mar 24, 2026

This PR has been updated and these are the updates:

1. Alignment on Correctness (Form State Cleanup) with video proof attached in description.

I have implemented a robust state management system using unregister() within the recursive SchemaField dispatcher.

  • The Fix: When a user switches branches (e.g., in a oneOf or anyOf selection), all stale/ghost values from the non-active branches are now automatically removed from the form state.
  • Submit Payload Consistency: This ensures that the final payload sent to the APISIX backend strictly adheres to the schema of the currently selected plugin variant, preventing hidden validation errors from inactive fields.

2. Verifiable Deliverables

I’ve updated my proposal to bind each phase to these deliverables:

  • Test Suite: Expanded to 47 passing tests (vitest) covering conditional branch switching and recursive validation.
  • Live Demo: Updated the schema_form_demo route with a "Live Form Data" monitor, allowing maintainers to visually verify that the payload is cleaned up in real-time as they toggle options.
  • Pass Criteria: 100% test pass rate + zero-warning pnpm lint build (verified with --max-warnings=0).

@DSingh0304
Copy link
Contributor Author

  • Optimized useWatch: Narrowed subscriptions in IfThenElseFields to only watch fields referenced in the if condition.

  • Recursive Cleanup: Added a collectAllPaths helper to ensure full subtree unregistration for nested objects/arrays.

  • Expanded Test Suite: Added 3 new test suites (totaling 50 tests) covering rapid branch switching, nested cleanup, and validation error purging.

@DSingh0304 DSingh0304 force-pushed the feat/json-schema-form-clean branch from 84aab84 to 56813fd Compare March 25, 2026 08:31
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.

GSoC 2026 Proposal: JSON Schema Driven Form Component [APISIX-39] Support JSONSchema to Form UI

4 participants