Generative UI: Build a Schema-Safe Renderer
Render model-defined interfaces safely through typed intent, validation, component allowlists, and deterministic actions.
Generative UI should not mean executing whatever interface a model imagines. The safe version is a small declarative language: model output describes intent, a runtime schema rejects invalid structures, and a capability-scoped registry renders known accessible components with deterministic actions.
This tutorial builds that boundary and attacks it with unknown components, unsafe URLs, invalid nesting, fake confirmations, and missing accessible names. The result is a renderer that can vary composition without surrendering semantics, security, or product identity.
The boundary combines schema-driven UI, an AI UI renderer, and a component allowlist; the formal specification appears beside the validation rules it supports.
Generative UI needs a small language
Define the jobs the model may compose: explain a result, collect a bounded input, compare options, or preview an action. Then expose a minimal node vocabulary such as text, callout, list, field, choice group, table, and action. Each node should correspond to a mature component with known responsive and accessibility behavior.
Avoid mirroring the full design-system prop surface. The model does not need pixel spacing, raw colors, arbitrary class names, or event handlers. Those values let generated output drift from the product and create injection sinks. Generative UI works best when the model chooses meaning while the renderer owns presentation.
My position is that the schema is an editorial medium. Its vocabulary should express the product’s strongest interaction patterns and omit weak ones. The boundary is not total sameness: layouts can vary through supported composition, content density, and data while still landing on a coherent visual grammar.
Write one example for every node and one example that combines nodes into a real task. The examples are not decoration; they define how the model should express hierarchy, labels, and consequence. Review them with design and engineering before expanding the vocabulary, because every admitted primitive becomes a long-lived capability.
Validate shape and semantic constraints
A closed schema can enforce node types, required fields, closed properties, string lengths, URL formats, and maximum tree depth. Add application validation for relationships the vocabulary cannot express clearly: a choice group needs at least two options, a table needs matching columns, and an action needs a visible consequence description.
Set budgets for node count, nesting, text length, and media. A valid but enormous tree can still freeze the interface or overwhelm a reader. Reject recursive patterns and cap repeated collections. Return typed issues with paths so the model can repair a candidate once without receiving permission to loop indefinitely.
The failure mode is permissive fallback: an unknown field or component is passed through to a generic HTML renderer. The consequence is an unreviewed capability surface. Mitigate it with additionalProperties: false, exhaustive dispatch, and a semantic error card that preserves safe text while omitting invalid behavior.
Validation should happen before server rendering and again at any process boundary where the intent object can change. Store the schema version with saved output. If a later renderer no longer supports that version, use an explicit archive view rather than coercing old content into new behavior and changing the meaning silently.
Illustrative TypeScript — an interface sketch, not a compiled implementation.
type UiNode =
| { type: "text"; style: "body" | "heading"; text: string }
| { type: "callout"; tone: "info" | "warning"; label: string; body: string }
| { type: "action"; actionId: string; label: string; consequence: string };
function assertNever(value: never): never {
throw new Error("Unsupported UI node");
}
Reference note. JSON Schema 2020-12 is the primary standard, model, or research source for the implementation claim immediately above.
| Signal | Decision | Proof |
|---|---|---|
| Unknown node | Render semantic fallback | Validation issue shown |
| Unsafe action | Deny binding | Policy reason logged |
| Missing name | Block interactive node | Accessible-tree test |
Scope components and actions separately
A component registry controls what can appear; an action registry controls what can happen. Do not let a model invent URLs, callback names, or tool arguments inside presentation JSON. An action node should reference a server-defined capability by ID, while validated contextual parameters come from the current application state.
In the worked invoice fixture, the model may render invoice details and propose ‘request correction.’ It cannot bind ‘delete invoice’ because that action is absent from the current scope. If it emits the unknown ID, the page shows the explanatory content, omits the control, and records a denied binding. The rest of the answer remains useful.
In generative UI, this separation also supports preview. The action registry can return a proposal containing recipient, resource, and consequence before commit. The renderer presents that typed proposal through a known confirmation component. A model-authored sentence never becomes the source of truth for what the button will do.
Bind every proposal to the intent digest and current resource revision. If data changes between preview and commitment, invalidate the proposal and request a fresh view. This prevents a well-designed confirmation from approving arguments that no longer match the material the person inspected.
Reference note. Trusted Types specification is the primary standard, model, or research source for the implementation claim immediately above.
Preserve accessible semantics by construction
Every interactive node needs a programmatic name and an appropriate native element. Heading levels should derive from document position, not a model-selected number. Field nodes require labels, hints, error association, and stable IDs generated by the renderer. Tables need headers and a linear fallback when the viewport cannot support columns.
Block an interactive node that lacks its accessible contract. Do not invent a vague label like ‘Action.’ A repair can ask the model for user-facing text, but the system should cap repair attempts and show safe fallback content. Generative UI should be no less accessible than a static page simply because composition is dynamic.
Test the accessible tree and keyboard order for every registry component, then add schema fixtures for invalid combinations. Automated checks catch missing names and roles; human review still evaluates whether the order, wording, and consequence make sense. Both levels belong in the component admission process.
At 200 percent zoom and narrow widths, content should reflow without horizontal page scrolling. Registry components may use internal scrolling for genuinely tabular data, but the reading order must remain useful without CSS. Motion and streamed insertion should respect reduced-motion settings and never steal focus from the active task.
Reference note. WCAG 2.2 is the primary standard, model, or research source for the implementation claim immediately above.
Keep rendering deterministic
Given the same schema version, registry version, data snapshot, and intent JSON, rendering should produce the same semantic tree. That property enables snapshots, hydration checks, replay, and debugging. Random layout selection or client-only repair makes a failed page hard to reconstruct and can change meaning after server render.
Record an intent digest, validation issues, registry versions, resolved node types, omitted capabilities, and action proposals. Redact sensitive field values. The receipt should answer why a component appeared and whether an action was actually bound, without storing every private response indefinitely.
Keep core prose and form controls server-renderable. Streaming can append validated subtrees at stable boundaries, but it should not replace the whole document on every token. This reduces layout shift and ensures the interface remains understandable when client JavaScript fails.
Separate content freshness from renderer determinism. Live data can update through a new snapshot and produce a new receipt; it should not mutate an already approved proposal invisibly. The interface can announce refreshed values, show the revision, and require another confirmation when consequence changed.
- GenerateGenerate
Produce versioned intent JSON, never source code.
- ValidateValidate
Reject shape, nesting, content, and URL violations.
- ResolveResolve
Map names to scoped components and actions.
- VerifyVerify
Inspect semantics, interaction, and effect receipts.
Attack the renderer with hostile fixtures
Create fixtures for an unknown script node, a javascript: URL, six levels of nesting, a table with no headers, an action with a fake confirmation message, an image with no description, and a field that attempts to submit hidden data. Each case should have an explicit render or rejection outcome.
Add a content attack where retrieved text asks the model to display a trusted-looking security banner and bind a transfer action. The schema may allow a warning callout, but the action registry denies the transfer and the interface labels generated content as an assistant response, not system policy. Composition must not confer authority.
Generative UI needs mutation tests too. Remove a required validator and confirm a fixture starts failing; swap a native button for a div and confirm accessibility checks fail. Those tests challenge the oracle rather than congratulating the implementation for matching snapshots it generated itself.
Run the same fixture at 360, 768, and 1440 pixels, with long translated labels and empty data. Capture the semantic tree, screenshot, validation result, and action registry receipt. A hostile test passes only when the rejected behavior stays rejected and the remaining content is still understandable.
Release the registry like an API
Version node contracts and maintain backward-compatible renderers for stored documents until they migrate. Deprecate components with usage evidence and a deterministic transform. If an old node cannot render safely, show a readable archive state instead of silently mapping it to a new behavior.
Monitor validation failure by path, denied action IDs, repair loops, render latency, layout shift, keyboard errors, and task completion. Review representative outputs for visual sameness and awkward composition. A perfectly safe registry can still become boring; new expressive primitives should enter through authored examples and the same accessibility bar.
The generative UI release receipt should include schema and registry hashes, fixture results, bundle size, three viewport screenshots, and one successful end-to-end action proposal. That evidence connects AI engineering to the crafted surface people actually use.
Review the generative UI vocabulary quarterly with actual outputs. Retire redundant nodes, improve primitives that repeatedly trigger repair, and add a component only when several useful compositions need it. The registry should evolve like a language: through observed expressive gaps, not isolated requests for visual novelty.
Conclusion and implementation references
Generative UI is safest when models choose from a small semantic language, validators enforce structure and budgets, registries control components and actions, and deterministic rendering leaves a receipt. Variability belongs inside an authored capability boundary.
Start with seven nodes and five hostile fixtures. Render one invoice explanation, deny an unavailable action, and inspect the accessible tree at mobile width. If that narrow slice holds, expand the vocabulary through reviewed components rather than arbitrary code.
This method also sits beside five related Journal notes: agent-readable product specs, component APIs that teach intent, reviewing AI-built screens, tests that challenge generated intent, UI PR risk review. Each expands one boundary that this article deliberately keeps narrow, so the links are supporting material rather than competing actions. The three authoritative references are placed beside the specific claims they support above; the framework, examples, failure modes, and implementation judgments are my synthesis.
Use the conclusion as a release boundary: reproduce the named fixture, preserve its evidence, and record any exception before extending the pattern to a higher-consequence workflow. That final receipt makes the method reviewable by someone who did not build it and gives a future update a concrete point of comparison.