acceptsChildren: false },
actionButton: { props: { text: 'string', style: 'string?', target: 'string?' }, acceptsChildren: false },
container: { props: { header: 'string?', theme: 'string?' }, acceptsChildren: true },
media: { props: { source: 'url', description: 'string?', ratio: 'string?' }, acceptsChildren: false },
tag: { props: { text: 'string', severity: 'string?' }, acceptsChildren: false },
separator: { props: {}, acceptsChildren: false },
flexSpace: { props: { weight: 'number?' }, acceptsChildren: false },
scrollList: { props: { gap: 'number?' }, acceptsChildren: true },
navigationLink: { props: { text: 'string', destination: 'url' }, acceptsChildren: false },
};
**Why this structure:** Using a `Record<string, ComponentDefinition>` enables O(1) lookups during validation and rendering. The `?` suffix convention for optional props keeps the schema compact while remaining machine-readable. Separating `acceptsChildren` from prop definitions prevents logical errors where leaf components accidentally receive nested payloads.
### Step 2: Build the Strict Validator
Validation must run before any DOM interaction. Errors should use JSONPath notation so server teams can trace exactly which node failed.
```typescript
interface ValidationError {
path: string;
message: string;
}
function getType(value: unknown): string {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
return typeof value;
}
export function validateLayoutNode(node: unknown, currentPath: string = '$'): ValidationError[] {
const errors: ValidationError[] = [];
if (node === null || typeof node !== 'object' || Array.isArray(node)) {
errors.push({ path: currentPath, message: 'Node must be a plain object' });
return errors;
}
const payload = node as Record<string, unknown>;
if (typeof payload.type !== 'string') {
errors.push({ path: currentPath, message: 'Missing required "type" field' });
return errors;
}
const definition = COMPONENT_CATALOG[payload.type];
if (!definition) {
errors.push({ path: currentPath, message: `Unregistered component: ${payload.type}` });
return errors;
}
for (const [key, typeRule] of Object.entries(definition.props)) {
const isOptional = typeRule.endsWith('?');
const baseType = isOptional ? typeRule.slice(0, -1) : typeRule;
const value = payload[key];
if (value === undefined) {
if (!isOptional) {
errors.push({ path: `${currentPath}.${key}`, message: `Required property missing` });
}
continue;
}
if (getType(value) !== baseType) {
errors.push({ path: `${currentPath}.${key}`, message: `Expected ${baseType}, received ${getType(value)}` });
}
}
if (definition.acceptsChildren) {
if (Array.isArray(payload.children)) {
payload.children.forEach((child, index) => {
errors.push(...validateLayoutNode(child, `${currentPath}.children[${index}]`));
});
} else if (payload.children !== undefined) {
errors.push({ path: `${currentPath}.children`, message: 'Children must be an array' });
}
} else if (payload.children !== undefined) {
errors.push({ path: `${currentPath}.children`, message: `Component ${payload.type} does not accept children` });
}
return errors;
}
Why JSONPath: Standard error messages like "missing text" are useless in production. A path like $.children[2].children[0].text allows the server team to pinpoint the exact payload fragment. This format is natively loggable, diffable, and integrates directly with error tracking pipelines.
Step 3: Implement the Dispatcher Renderer
The renderer maps validated types to DOM construction functions. Unknown types must never fail silently. A visible placeholder preserves layout structure and triggers error telemetry.
type RenderFunction = (payload: Record<string, unknown>) => HTMLElement;
const RENDER_DISPATCH_TABLE: Record<string, RenderFunction> = {
column(payload) {
const el = document.createElement('div');
el.className = 'sdui-column';
if (payload.gap != null) el.style.gap = `${payload.gap}px`;
if (payload.alignment) el.style.alignItems = payload.alignment as string;
(payload.children || []).forEach(child => el.appendChild(renderNode(child)));
return el;
},
row(payload) {
const el = document.createElement('div');
el.className = 'sdui-row';
if (payload.gap != null) el.style.gap = `${payload.gap}px`;
if (payload.alignment) el.style.alignItems = payload.alignment as string;
(payload.children || []).forEach(child => el.appendChild(renderNode(child)));
return el;
},
label(payload) {
const el = document.createElement('span');
el.className = 'sdui-label';
el.textContent = payload.text as string;
if (payload.emphasis) el.classList.add(`sdui-emphasis-${payload.emphasis}`);
return el;
},
// Additional renderers follow the same pattern...
};
function renderNode(payload: unknown): HTMLElement {
if (typeof payload !== 'object' || payload === null || !('type' in payload)) {
const fallback = document.createElement('div');
fallback.className = 'sdui-invalid';
fallback.textContent = '[Invalid Node]';
return fallback;
}
const type = (payload as Record<string, unknown>).type as string;
const renderer = RENDER_DISPATCH_TABLE[type];
if (!renderer) {
const fallback = document.createElement('div');
fallback.className = 'sdui-unknown';
fallback.textContent = `[Unknown: ${type}]`;
console.warn(`SDUI: Unregistered component "${type}" encountered at runtime.`);
return fallback;
}
return renderer(payload as Record<string, unknown>);
}
Why explicit fallbacks: Silent skipping causes partial UI disappearance. Users report broken screens, but developers see no errors. Rendering [Unknown: type] preserves the DOM tree, highlights the issue visually, and guarantees the error reaches monitoring systems.
Step 4: Wire the Execution Pipeline
The engine orchestrates validation, metrics collection, and rendering. Keeping validation DOM-free ensures the same logic works across web, React Native, or native mobile bridges.
export interface LayoutMetrics {
totalNodes: number;
maxDepth: number;
}
function collectMetrics(node: unknown, currentDepth: number = 0): LayoutMetrics {
if (typeof node !== 'object' || node === null) return { totalNodes: 0, maxDepth: 0 };
const payload = node as Record<string, unknown>;
let depth = currentDepth;
let count = 1;
if (Array.isArray(payload.children)) {
payload.children.forEach(child => {
const childMetrics = collectMetrics(child, currentDepth + 1);
count += childMetrics.totalNodes;
depth = Math.max(depth, childMetrics.maxDepth);
});
}
return { totalNodes: count, maxDepth: depth };
}
export function executeLayout(payload: unknown): HTMLElement {
const validationErrors = validateLayoutNode(payload);
if (validationErrors.length > 0) {
throw new Error(`Layout validation failed:\n${validationErrors.map(e => `${e.path}: ${e.message}`).join('\n')}`);
}
const metrics = collectMetrics(payload);
if (metrics.maxDepth > 15 || metrics.totalNodes > 500) {
console.warn(`SDUI: Layout exceeds safe bounds (depth: ${metrics.maxDepth}, nodes: ${metrics.totalNodes}). Consider flattening.`);
}
return renderNode(payload);
}
Why metrics matter: Unbounded JSON payloads can trigger stack overflows or render lag. Enforcing depth and node limits at the engine level prevents denial-of-service scenarios and keeps client performance predictable.
Pitfall Guide
1. Silent Fallbacks on Unknown Types
Explanation: When the server emits a component type the client doesn't recognize, skipping it silently removes UI elements without warning. Users see broken layouts; developers see no errors.
Fix: Always render a visible placeholder ([Unknown: type]) and emit a structured warning to your error tracking pipeline. Treat unknown types as high-severity incidents.
2. Catalog Bloat (The "God Protocol")
Explanation: Teams gradually add bespoke components for every edge case. The catalog grows to 100+ types, each with unique props. Maintenance becomes impossible, and cross-platform parity degrades.
Fix: Cap the catalog at 20β30 primitives. Enforce composition: new layouts should combine existing widgets, not introduce new ones. Adding a widget requires a full client release and API version bump.
3. Vague Validation Errors
Explanation: Returning generic messages like "missing field" forces server teams to guess which node failed in a deeply nested payload.
Fix: Use JSONPath-style paths ($.children[2].label). This format is natively supported by most logging systems and enables automated payload debugging.
4. Ignoring Versioning Contracts
Explanation: Treating the SDUI payload as a free-form configuration leads to breaking changes. Clients crash when receiving new props or types they don't understand.
Fix: Treat the catalog as a versioned API. Increment the contract version when adding types or changing required props. Implement client-side version negotiation or fallback strategies.
5. Tying Validation to the DOM
Explanation: Embedding validation logic inside rendering functions couples the contract to a specific platform. You lose the ability to validate payloads in CI, on the server, or in native bridges.
Fix: Keep validation pure and DOM-agnostic. The validator should accept a plain object and return structured errors. Rendering should only occur after validation passes.
6. Over-Parameterizing Components
Explanation: Adding style, animation, and layout props to every component turns the payload into a CSS-in-JSON blob. This defeats the purpose of native rendering and increases payload size.
Fix: Limit props to semantic and structural needs. Delegate styling to client-side themes or design tokens. Keep the payload focused on hierarchy, content, and interaction targets.
7. Skipping Depth and Node Metrics
Explanation: Malformed or malicious payloads can trigger infinite recursion or render thousands of nodes, freezing the client.
Fix: Collect maxDepth and totalNodes during validation. Enforce hard limits (e.g., depth β€ 15, nodes β€ 500). Reject or truncate payloads that exceed thresholds.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Marketing campaigns / promo banners | Lightweight SDUI with dynamic payloads | Zero client updates required; instant rollout | Low (JSON payload + CDN caching) |
| Core navigation / persistent UI | Client-side native implementation | Stability and performance outweigh remote flexibility | Medium (Initial dev + maintenance) |
| Dynamic feeds / content lists | SDUI with scrollList + card composition | Server controls hierarchy; client handles rendering | Low-Medium (Payload parsing + layout engine) |
| Complex interactive forms | Hybrid: SDUI for layout + client-side validation | Forms require tight state management and accessibility guarantees | Medium (Dual validation logic) |
| Emergency UI hotfixes | SDUI with versioned fallback | Server swaps payload; client renders known types | Low (Immediate mitigation) |
Configuration Template
// sdui-config.ts
import { COMPONENT_CATALOG, validateLayoutNode, executeLayout } from './sdui-engine';
export const SDUI_CONFIG = {
catalog: COMPONENT_CATALOG,
validation: {
strictMode: true,
maxDepth: 15,
maxNodes: 500,
errorFormatter: (errors: Array<{path: string, message: string}>) =>
errors.map(e => `${e.path}: ${e.message}`).join('\n')
},
rendering: {
unknownTypePlaceholder: (type: string) => `[Unknown: ${type}]`,
logUnknownTypes: true,
attachTelemetry: (payload: unknown, metrics: {totalNodes: number, maxDepth: number}) => {
// Send to your analytics/error pipeline
console.info('SDUI Render Metrics:', metrics);
}
}
};
export function initializeSDUI(container: HTMLElement, payload: unknown): void {
try {
const element = executeLayout(payload);
container.innerHTML = '';
container.appendChild(element);
} catch (error) {
console.error('SDUI Execution Failed:', error);
container.innerHTML = '<div class="sdui-error">Layout failed to render. Contact support.</div>';
}
}
Quick Start Guide
- Install the engine: Copy the catalog, validator, renderer, and execution pipeline into your project. Ensure TypeScript is configured for strict mode.
- Define your catalog: Populate
COMPONENT_CATALOG with 10β15 primitives matching your design system. Mark required props explicitly.
- Wire validation: Call
validateLayoutNode() on incoming payloads before rendering. Log JSONPath errors to your server-side monitoring.
- Attach the renderer: Mount
executeLayout() to a container element. Handle unknown types with visible placeholders and telemetry.
- Test with edge cases: Validate missing props, unknown types, excessive depth, and malformed children. Verify that errors surface in your logging pipeline and unknown types render placeholders.