# Script parameter / return: JSON Schema + JSONJoy editor

**Date:** 2026-05-12
**Status:** Superseded — see "Reversal: native Blazor SchemaBuilder" below.

## Decision

Replace the custom `ParameterListEditor` / `ReturnTypeEditor` Blazor components
with [`jsonjoy-builder`](https://github.com/lovasoa/jsonjoy-builder) (`SchemaVisualEditor`),
embedded as a React island. The on-disk format for `TemplateScript.ParameterDefinitions`
and `TemplateScript.ReturnDefinition` changes from the project-local flat shape
(`[{name,type,required,itemType?}]` / `{type,itemType?}`) to standard JSON Schema.

## Rationale

The existing flat shape lacked descriptions, defaults, enums, nested objects,
and arrays of structured items. JSON Schema covers all of that, is the
industry vocabulary other tooling already speaks (OpenAPI 3.1, function-calling
APIs, validators), and `jsonjoy-builder` is a polished pre-built visual editor
for it.

## Trade-offs

- **Breaks the no-UI-framework rule for this feature.** `jsonjoy-builder` is
  React 19 + Radix UI + Tailwind. Accepted: the island is isolated to one
  modal panel, Tailwind is shipped pre-built (no toolchain shared with the
  Blazor side), and the visual delta is contained.
- **New build pipeline.** A small Vite project under `src/ZB.MOM.WW.ScadaBridge.CentralUI/Schema.Editor/`
  builds a single IIFE bundle into `wwwroot/lib/schema-editor/`. Output is
  committed so `dotnet build` doesn't require Node.
- **Monaco overlap.** `jsonjoy-builder` depends on `@monaco-editor/react`,
  which depends on `monaco-editor`. We already load Monaco globally for the
  script code editor. The island calls `@monaco-editor/react`'s `loader.config({ monaco: window.monaco })`
  at boot to reuse the same instance — no duplicate Monaco download.

## Storage format change

| Field                  | Before                            | After                                                       |
| ---------------------- | --------------------------------- | ----------------------------------------------------------- |
| `ParameterDefinitions` | `[{name,type,required,itemType?}]` | `{"type":"object","properties":{...},"required":[...]}`    |
| `ReturnDefinition`     | `{type,itemType?}`                | Any JSON Schema (root `type` describes the returned value) |

Per the chosen rollout: **one-shot migration** rewrites all existing rows on
deploy. After the migration, the analysis pipeline reads JSON Schema only —
no dual-format support code.

Type mapping (flat → JSON Schema):

| Flat type | JSON Schema |
| --------- | ----------- |
| `Boolean` | `{"type":"boolean"}` |
| `Integer` | `{"type":"integer"}` |
| `Float`   | `{"type":"number"}` |
| `String`  | `{"type":"string"}` |
| `Object`  | `{"type":"object"}` |
| `List` of X | `{"type":"array","items":{"type":<X>}}` |

`required: false` ⇒ name omitted from the `required` array.
`required: true` (default) ⇒ name added to `required`.

## Component layout

```
src/ZB.MOM.WW.ScadaBridge.CentralUI/Schema.Editor/        ← new Vite project (committed)
  package.json
  vite.config.ts
  tsconfig.json
  src/main.tsx                                ← exposes window.ScadaSchemaEditor
  src/SchemaEditorApp.tsx
  src/index.css
  .gitignore                                  ← node_modules only
  dist/                                       ← (Vite outputs to wwwroot, not here)

src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/lib/schema-editor/
  schema-editor.js                            ← built IIFE, committed
  schema-editor.css

src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/
  SchemaEditor.razor                          ← Blazor wrapper; mirrors MonacoEditor.razor

src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/
  ScriptShapeParser.cs                        ← rewrite to read JSON Schema
src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/
  ScriptParameterNames.cs                     ← rewrite to read JSON Schema
```

Removed after rollout: `ParameterListEditor.razor`, `ReturnTypeEditor.razor`.

## JS interop contract

```ts
window.ScadaSchemaEditor = {
  mount(id: string, host: HTMLElement, options: {
    value: string;          // current schema JSON (may be empty)
    mode: 'parameters' | 'return';
    readOnly?: boolean;
  }, dotNetRef: { invokeMethodAsync(name: 'OnValueChanged', json: string): Promise<void> }): void;
  setValue(id: string, value: string): void;
  dispose(id: string): void;
}
```

## Migration

EF Core migration in `ZB.MOM.WW.ScadaBridge.ConfigurationDatabase` reads
`TemplateScripts.ParameterDefinitions` and `ReturnDefinition` from every row,
sniffs format (array vs object), translates if legacy, writes back. Idempotent:
re-running a row already in JSON Schema is a no-op. Runs once at deploy via
the existing auto-apply path.

## Out of scope (deferred)

- Schema-driven value-entry forms (e.g. Inbound API tester) — would also use
  `jsonjoy-builder`'s value-editor mode, but no caller surface needs it today.
- Hover/completion enhancements derived from JSON Schema descriptions or
  defaults. Today's pipeline only needs name + type + required.
- Reuse of JSON Schema `$ref` across templates — could be a future template-level
  schema library.

---

## Reversal: native Blazor SchemaBuilder (2026-05-12, same day)

JSONJoy worked but felt heavy for the actual data we author here. Specifically:

- The "Add Field" modal flow is two clicks per parameter where the legacy
  inline-row editor was zero. For the common 1-3 scalar-param case, a visible
  modal dialog every time is friction.
- JSONJoy's value-mode UX is awkward — it always renders an "Add Field" button
  even when the schema's root type is `string` / `integer` / etc., so the
  Return-type tab is mismatched to the underlying single-value model.
- React 19 + Radix + Tailwind for one form field is a lot of build pipeline
  surface to maintain.

**Decision:** replace JSONJoy with a Bootstrap-only Blazor component
(`SchemaBuilder.razor`) that recurses through its own render methods.
Storage format unchanged — still JSON Schema. The migration, parser, and
downstream analysis code are untouched.

**Scope decisions (from refinement session):**

- Type set: only the six JSON Schema primitives
  (`string · integer · number · boolean · object · array`). No `date-time` /
  `format`, no `enum` / `pattern` / `min/max`, no `$ref` / `oneOf` /
  `anyOf` / `allOf`, no `additionalProperties`. Power-user expansion can
  come later behind a per-row "more options" toggle.
- No description support per property. The row stays a single horizontal
  line: name + type + (items: type if array) + required + remove.
- Nested objects and arrays-of-objects recurse — same editor renders at any
  depth.

**Files added:**

- `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/SchemaBuilderModel.cs` —
  in-memory `SchemaNode` / `SchemaProperty` tree plus pure-static
  parse / serialize. Round-trips through the canonical JSON Schema text and
  tolerates legacy flat-array shape as a parse fallback.
- `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/SchemaBuilder.razor` —
  recursive renderer driven by `Mode="object"` (parameter list) or
  `Mode="value"` (single value, with object/array falling back to the
  property editor).
- `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/SchemaBuilderModelTests.cs` —
  parse / serialize / round-trip / legacy-array coverage.

**Files removed:**

- `src/ZB.MOM.WW.ScadaBridge.CentralUI/Schema.Editor/` (Vite project, node_modules, etc.)
- `src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/lib/schema-editor/` (built bundle)
- `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/SchemaEditor.razor` (Blazor wrapper)
- `<script>` / `<link>` references to schema-editor in `App.razor`
- `<DefaultItemExcludes>Schema.Editor/**` from CentralUI csproj

**Forms updated:** `TemplateEdit.razor`, `SharedScriptForm.razor`,
`ApiMethodForm.razor` now use `<SchemaBuilder>` directly.

The original `jsonjoy-builder` integration sections above are kept for
historical context but no longer reflect what's in the codebase.
