Mingqi Hou

Schema-Driven UI: From Hard-Coded Screens to Configurable Platforms

How I render forms, tables, and pages from JSON schema—component maps, dynamic loaders, and where permissions plug in.

Hard-coded screens ship fast once. The tenth variant of “almost the same form” is where products stall. On admin and operations products I move repeatable UI to schema-driven rendering—engineers own the map and guardrails; product owns JSON (or a visual editor) for fields, layout, and actions.

Where configuration wins

SurfaceConfig might describe
FormFields, validation, visibility rules, defaults
TableColumns, formatters, row actions
PageSections, tabs, nested blocks
ActionsButton type, dialog, API binding
CampaignsTime windows, rewards, rule expressions

Goal: launch a new operational flow without a frontend release when risk allows.

Core idea

Express the screen as data:

{
  "type": "form",
  "fields": [
    {
      "type": "input",
      "label": "Username",
      "field": "username",
      "required": true
    },
    {
      "type": "select",
      "label": "Role",
      "field": "role",
      "options": [
        { "label": "Admin", "value": "admin" },
        { "label": "User", "value": "user" }
      ]
    }
  ]
}

Mount one renderer:

<DynamicForm config={formConfig} onSubmit={handleSubmit} />

The engine maps type → React component, wires validation and events, and stays ignorant of business nouns except through schema.

Renderer architecture

interface ConfigSchema {
  type: "form" | "table" | "page";
  fields?: FieldSchema[];
  columns?: ColumnSchema[];
  layout?: LayoutBlock[];
}

const registry: Record<string, React.ComponentType<FieldProps>> = {
  input: TextField,
  select: SelectField,
  date: DateField,
  // ...
};

function renderField(field: FieldSchema) {
  const Component = registry[field.type];
  if (!Component) throw new Error(`Unknown field type: ${field.type}`);
  return <Component key={field.field} {...field} />;
}

Patterns I always add:

Dynamic loading (when bundles grow)

const loaders = {
  richText: () => import("./fields/RichTextField"),
  chart: () => import("./fields/ChartField"),
};

const Component = lazy(loaders[field.type]);

Keeps initial admin bundle smaller; heavy widgets load only when schema requests them.

Permissions and config

Bind action visibility to the same codes as RBAC:

{
  "type": "button",
  "label": "Export",
  "action": "exportUsers",
  "permission": "btn.user.export"
}

The renderer skips nodes the user cannot run—server still enforces.

Pitfalls

PitfallMitigation
Schema becomes a second programming languageLimit expressiveness; code escape hatches for rare cases
No validation of JSONJSON Schema + CI fixtures per screen
Performance on huge tablesVirtualization, server pagination
Designer/PM edits break prodStaging preview + audit log

Relation to AI-assisted delivery

On teams using SDD + Rules + Skills, schema files are excellent spec artifacts: the agent implements or extends DynamicForm, humans edit JSON for the rollout. That is how I keep velocity without surrendering type safety on the engine itself.

Configurable UI is the bridge between “React contractor” and “platform engineer”—relevant for Upwork clients who outgrow one-off landing pages and need operable internal tools.