Schema-Driven UI Development in Meshery

Meshery aims to decouple the UI logic from hardcoded structures and become fully schema-driven. This allows the UI to dynamically adapt based on changes in the underlying schema without requiring constant manual updates. This document explains how to integrate schema definitions from the meshery/schemas repository into the Meshery UI using a code-generation approach.


Overview

Meshery’s schemas define the structure of data (constructs) using JSON Schema and their behavior (API operations) using OpenAPI specifications. From these definitions, Meshery auto-generates:

  • Go structs for backend validation and API handling
  • TypeScript types and objects for frontend development
  • Templates in JSON and YAML formats

This approach ensures the schemas remain the single source of truth.


Repository Structure

All schema definitions live in the meshery/schemas repository.

schemas/
  constructs/
    v1beta1/
      design/
        design.json              # JSON schema for the noun
        design_template.json     # JSON template (generated)
        design_template.yaml     # YAML template (generated)
        openapi.yml               # OpenAPI spec defining operations
        subschemas/               # Optional reusable schema parts
  • <construct>.json: Defines structure (noun) — e.g., what a Design looks like.
  • openapi.yml: Describes operations (verbs) — e.g., how to GET, POST, or DELETE a design.
  • Templates: Valid, default-filled, resolved objects from the schema.

Schema-Driven UI Development Workflow

Meshery follows a schema-first approach where the UI is driven by JSON schemas and OpenAPI specifications. Here’s how you can contribute to and use these schemas in the Meshery UI.


Step 1: Define or Update the Schema

If you’re introducing or modifying a UI feature that requires a new schema:

  1. Navigate to the appropriate schema directory:

    schemas/constructs/<version>/<construct>/
    
  2. Define the schema structure in <construct>.json using JSON Schema v7.

  3. Define API operations related to this schema in openapi.yml using OpenAPI v3. This helps generate consistent API types and models that align with your UI needs.


Step 2: Generate TypeScript Types and Schema Objects

Use the following command to generate TypeScript types and JavaScript schema objects:

make generate-types

This will:

  • Generate .ts and .d.ts files under:

    typescript/constructs/<version>/<construct>/
    
  • Create:

    • Typed interfaces for schema validation and component props
    • Schema objects usable for dynamic UI (e.g., forms, validations)

Step 3: Build and Export Types/Schema Objects

After generation:

  1. Open typescript/index.ts

    • Export the newly generated types and schema objects from here.

    • Example:

      export * from "./constructs/v1beta1/design";
      
  2. Build the TypeScript package to make the changes usable:

    npm run build
    

Step 4: Use Schema Package in UI

To consume the schema in the UI:

  1. Install the schema package locally:

    npm install <path-to-schemas-repo>
    

    Examples:

    • Relative path:

      npm install ../../schemas
      
    • Absolute path:

      npm install /home/user/code/schemas
      
  2. This will update your package.json to something like:

    "@meshery/schema": "file:../../schemas"
    
  3. Now you can import types and schema objects in your UI components:

Example: Importing and Using a Type

import { DesignTypes } from "@meshery/schema";

// Type safety!
const renderDesignCard = (design: Design) => <div>{design.name}</div>;

Example: Accessing Runtime Schema

import { DesignSchema } from "@meshery/schema";

const validateDesign = (data) => {
  const isValid = ajv.validate(DesignSchema, data);
  return isValid;
};

Integration Points in UI

A. Generated RTK Query Client

@meshery/schemas publishes more than types: @meshery/schemas/mesheryApi is a generated RTK Query client with a hook per API operation. ui/rtk-query/index.ts re-exports that client as api, so every module under ui/rtk-query/ injects into the same instance and the generated hooks are already available from those modules.

Consume the generated hook. Do not re-declare the request. A hand-written builder.query/builder.mutation for an operation schemas already defines forks the wire contract silently, and Meshery and Layer5 Cloud drift apart with nothing failing.

// Correct - the generated hook, imported directly.
import { useGetWorkspacesQuery } from "@meshery/schemas/mesheryApi";

A local module is the right place only for ergonomics over a generated endpoint - adapting an argument shape, or attaching a Meshery-local cache tag:

// Argument adaptation: callers pass a bare id, the generated endpoint takes an object.
export const useGetCredentialByIdQuery = (credentialId: string, options?: object) =>
  useSchemasGetCredentialByIdQuery({ credentialId }, options);
// Cache tags: the callback form of enhanceEndpoints APPENDS to the generated tags.
// The object form Object.assigns over them and drops every schemas-side tag.
import { appendInvalidatesTags } from "./utils";

api.enhanceEndpoints({
  addTagTypes: [TAGS.DESIGNS],
  endpoints: {
    importDesign: appendInvalidatesTags("importDesign", { type: TAGS.DESIGNS }),
  },
});

A local endpoint that shadows a generated one is dead code

injectEndpoints silently discards an endpoint whose name the generated client already defines - a dev-only console warning, nothing more - and serves every call from the generated definition. Neither the typecheck nor a test that only asserts a hook exists will notice, so a local declaration can look authoritative while a different request goes over the wire. Meshery has shipped user-visible breakage this way: callers shaped for the discarded local definition sent undefined where the generated endpoint expected an id.

Before adding an endpoint, check the name against the generated client:

grep '<operationId>:t\.' ui/node_modules/@meshery/schemas/dist/mesheryApi.js

Override deliberately only when the generated operation is genuinely wrong for Meshery today - for example when schemas has landed a path the server has not adopted yet. Set overrideExisting: true explicitly, explain why in a comment, and link the meshery/schemas issue tracking the divergence - see ui/rtk-query/notificationCenter.ts and meshery/schemas#1134.

Test the effective endpoint, not the declared one: dispatch through a real store and assert the URL, method and body. A test that reads the module’s source shape, or that calls fetch itself and then asserts its own mock, proves nothing. The guard tests to copy are ui/rtk-query/__tests__/{workspace-mutation-wrappers,notificationCenter-effective-endpoints}.test.ts*; where a local hook only adapts an argument shape, drive that hook with renderHook so the adaptation itself is exercised rather than bypassed.


B. RJSF JSON Schemas

Meshery uses react-jsonschema-form to render forms dynamically based on JSON schemas. All of Meshery’s RJSF schemas are defined in the @sistent/sistent package, which extends schemas from the @meshery/schema package.

This approach enables us to generate forms that automatically adapt to the schema structure without hardcoding field properties like type, enum, description, and others.

import { DesignSchema } from "@meshery/schema";

const designSchema = {
  ...DesignSchema,
  properties: {
    ...DesignSchema.properties,
    name: {
      ...DesignSchema.properties.name,
      description: DesignSchema.properties.description,
      ui: {
        label: DesignSchema.properties.name.title,
        placeholder: DesignSchema.properties.name.description,
      },
    },
    // Other properties with UI-specific enhancements
  },
};

C. General Form UI

OpenAPI schemas (especially request bodies for POST/PUT operations) serve as the foundation for building form logic. These definitions include:

  • Field validations (e.g. required, format, maxLength)
  • Field types and formats (e.g. string, integer, date-time)
  • Descriptions and examples
  • Enum constraints and conditional logic
  • Custom extensions like x-rjsf-* for layout

This ensures alignment between frontend form behavior and backend expectations.

import { DesignTypes } from "@meshery/schema";

const DesignForm = ({ design }: { design: DesignTypes }) => (
  <form>
    <input type="text" value={design.name} />
    {/* More fields derived from the schema */}
  </form>
);

D. UI-Specific Descriptions and Enhancements

Any UI-specific metadata—such as name, type, hints, descriptions, defaults, etc.—is defined directly within the relevant schema object. Elements like tooltips, descriptions, and other metadata are frequently needed across the UI, so having a single source of truth in the schema object ensures consistency and reduces duplication.

For example, if we have a Design schema, the UI retrieves details like the design’s name, description, and other properties directly from the schema object.


E. Type Safety for Component Props

Generated TypeScript types from the schema ensure UI components are type-safe and consistent with backend contracts.

import { DesignTypes } from "@meshery/schema";

const DesignCard = ({ design }: { design: DesignTypes }) => (
  <div>
    <h2>{design.name}</h2>
    <p>{design.description}</p>
  </div>
);