1. The core idea
This experiment uses seven small files plus App.tsx. The JSON never creates arbitrary React code. Instead, it declares a type; Zod validates that declaration; and a renderer maps the approved type to one of your trusted React components.
JSON
│
▼
Zod validation
│
▼
UIRenderer
│
├── "text" → <Text />
├── "card" → <Card />
├── "alert" → <Alert />
├── "table" → <Table />
└── "button" → <Button />2. Create the project folders
src/
├── components/
│ ├── Text.tsx
│ ├── Card.tsx
│ ├── Alert.tsx
│ ├── Table.tsx
│ └── Button.tsx
├── generative-ui/
│ └── UIRenderer.tsx
├── schemas/
│ └── uiSchema.ts
├── App.tsx
└── main.tsxThe source code for this Generative UI learning project is being maintained in the public repository vpereira3000/generative_ui. Readers can use the repository as the companion implementation while following the tutorial.
3. Create the Text component
Create src/components/Text.tsx:
type TextProps = {
content: string;
};
export function Text({ content }: TextProps) {
return <p>{content}</p>;
}This simple component accepts:
{
"content": "Hello from Generative UI"
}4. Create the Card component
Create src/components/Card.tsx:
type CardProps = {
title: string;
description: string;
};
export function Card({
title,
description,
}: CardProps) {
return (
<div
style={{
border: "1px solid #ccc",
borderRadius: "8px",
padding: "16px",
marginBottom: "12px",
}}
>
<h2>{title}</h2>
<p>{description}</p>
</div>
);
}{
"type": "card",
"props": {
"title": "Oracle Database",
"description": "Enterprise relational database platform."
}
}5. Create the Alert component
Create src/components/Alert.tsx:
type AlertProps = {
title: string;
message: string;
};
export function Alert({
title,
message,
}: AlertProps) {
return (
<div
style={{
border: "1px solid orange",
borderRadius: "8px",
padding: "16px",
marginBottom: "12px",
}}
>
<strong>{title}</strong>
<p>{message}</p>
</div>
);
}{
"type": "alert",
"props": {
"title": "Database Warning",
"message": "CPU usage reached 95%."
}
}6. Create the Button component
Create src/components/Button.tsx:
type ButtonProps = {
label: string;
};
export function Button({
label,
}: ButtonProps) {
return (
<button
onClick={() => alert(`Clicked: ${label}`)}
style={{
padding: "10px 16px",
cursor: "pointer",
}}
>
{label}
</button>
);
}7. Create the Table component
Create src/components/Table.tsx:
type TableProps = {
columns: string[];
rows: string[][];
};
export function Table({
columns,
rows,
}: TableProps) {
return (
<table
style={{
borderCollapse: "collapse",
width: "100%",
marginBottom: "16px",
}}
>
<thead>
<tr>
{columns.map((column) => (
<th
key={column}
style={{
border: "1px solid #ccc",
padding: "8px",
}}
>
{column}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, rowIndex) => (
<tr key={rowIndex}>
{row.map((cell, cellIndex) => (
<td
key={cellIndex}
style={{
border: "1px solid #ccc",
padding: "8px",
}}
>
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}Example data:
{
"type": "table",
"props": {
"columns": ["Database", "Type"],
"rows": [
["Oracle", "Commercial"],
["PostgreSQL", "Open Source"]
]
}
}8. Define the Zod UIComponent schema
Create src/schemas/uiSchema.ts:
import { z } from "zod";
const TextSchema = z.object({
type: z.literal("text"),
props: z.object({
content: z.string(),
}),
});
const CardSchema = z.object({
type: z.literal("card"),
props: z.object({
title: z.string(),
description: z.string(),
}),
});
const AlertSchema = z.object({
type: z.literal("alert"),
props: z.object({
title: z.string(),
message: z.string(),
}),
});
const ButtonSchema = z.object({
type: z.literal("button"),
props: z.object({
label: z.string(),
}),
});
const TableSchema = z.object({
type: z.literal("table"),
props: z.object({
columns: z.array(z.string()),
rows: z.array(
z.array(z.string())
),
}),
});
export const UIComponentSchema =
z.discriminatedUnion("type", [
TextSchema,
CardSchema,
AlertSchema,
ButtonSchema,
TableSchema,
]);
export type UIComponent =
z.infer<typeof UIComponentSchema>;This gives us runtime validation plus TypeScript type safety from one definition.
9. Understand the discriminator
z.discriminatedUnion("type", ...) tells Zod to inspect type and select the matching schema.
{ "type": "card" }{ "type": "table" }10. Create the dynamic renderer
Create src/generative-ui/UIRenderer.tsx:
import type { UIComponent } from "../schemas/uiSchema";
import { Text } from "../components/Text";
import { Card } from "../components/Card";
import { Alert } from "../components/Alert";
import { Button } from "../components/Button";
import { Table } from "../components/Table";
type UIRendererProps = {
component: UIComponent;
};
export function UIRenderer({
component,
}: UIRendererProps) {
switch (component.type) {
case "text":
return <Text {...component.props} />;
case "card":
return <Card {...component.props} />;
case "alert":
return <Alert {...component.props} />;
case "button":
return <Button {...component.props} />;
case "table":
return <Table {...component.props} />;
default:
return null;
}
}This switch is effectively our first component registry.
11. Test the renderer with JSON
Replace src/App.tsx with:
import {
UIComponentSchema,
} from "./schemas/uiSchema";
import { UIRenderer } from "./generative-ui/UIRenderer";
function App() {
const jsonData = {
type: "card",
props: {
title: "Oracle Database 26ai",
description:
"A database platform with integrated AI and vector capabilities.",
},
};
const result =
UIComponentSchema.safeParse(jsonData);
if (!result.success) {
return (
<div>
<h1>Invalid UI definition</h1>
<pre>
{JSON.stringify(
result.error,
null,
2
)}
</pre>
</div>
);
}
return (
<div
style={{
maxWidth: "800px",
margin: "40px auto",
fontFamily: "Arial",
}}
>
<h1>Generative UI Lab</h1>
<UIRenderer
component={result.data}
/>
</div>
);
}
export default App;Start the application:
npm run devOpen http://localhost:5173. The JSON contains type: "card", so the renderer selects <Card />.
12. Prove that data selects the interface
Do not modify the component files, renderer, or schema. Change only jsonData:
const jsonData = {
type: "alert",
props: {
title: "Database Warning",
message: "CPU usage has reached 95%.",
},
};Vite hot reloads. React now renders an Alert instead of a Card.
13. Test Table, Button and Text
Table
const jsonData = {
type: "table",
props: {
columns: [
"Database",
"License",
"Vector Support",
],
rows: [
["Oracle", "Commercial", "Yes"],
["PostgreSQL", "Open Source", "Yes"],
["MySQL", "Open Source", "Yes"],
],
},
};Button
const jsonData = {
type: "button",
props: {
label: "Analyze Database",
},
};Text
const jsonData = {
type: "text",
props: {
content:
"Welcome to our first Generative UI experiment.",
},
};14. Prove that Zod protects the application
Now intentionally send invalid props:
const jsonData = {
type: "card",
props: {
title: 12345,
description: true,
},
};The schema expects strings, so UIComponentSchema.safeParse(jsonData) fails and your application displays Invalid UI definition instead of attempting to render bad data.
15. Reject an unauthorized component
const jsonData = {
type: "videoPlayer",
props: {
url: "something.mp4",
},
};Because videoPlayer does not exist in the discriminated union, Zod rejects the object.
Terminal, FileDelete, ExecuteShell, DatabaseDrop, or RunJavaScript unless you explicitly expose and validate them.16. What we built
JSON
│
▼
UIComponentSchema
Zod
│
valid / invalid
│
▼
UIRenderer
│
inspect component.type
│
┌──────────┼──────────┐
▼ ▼ ▼
"card" "table" "alert"
│ │ │
▼ ▼ ▼
<Card /> <Table /> <Alert />
│
▼
Browser17. Why this is already Generative UI
Today, you manually write:
{
"type": "table",
"props": { ... }
}Later, an LLM will generate the same declarative UI description. The frontend architecture does not need to change.
User
│
▼
LLM
│
▼
Structured UI JSON
│
▼
Zod
│
▼
UIRenderer
│
▼
React componentThe LLM does not generate React. It generates a declarative description of the interface. Your application owns the real components.
18. Follow the implementation on GitHub
This tutorial is part of a public, incremental Generative UI learning project. Readers can inspect the repository, clone it, or download the current source as a ZIP:
Open vpereira3000/generative_ui on GitHub →
Download the main branch as ZIP →
git clone https://github.com/vpereira3000/generative_ui.git
cd generative_uiAs each tutorial is implemented and committed, GitHub becomes the direct inspection point for the exact source files discussed in the article.
19. Final proof
Same React application
+
Same renderer
+
Same trusted components
↓
Different validated JSON
↓
Different interfaceYou have implemented the basic mechanism behind Generative UI: declarative intent, runtime validation, a trusted component vocabulary, and dynamic rendering.