DEV Community

Cover image for From LLM JSON to Real UI: Building Safer AI Interfaces with GenUIKit
Mohammad Ashrafian
Mohammad Ashrafian

Posted on

From LLM JSON to Real UI: Building Safer AI Interfaces with GenUIKit

Most AI UI demos look great right up until the model returns something slightly wrong.

Maybe the component name is not valid.
Maybe a required prop is missing.
Maybe an enum value is close, but still wrong.
Maybe the JSON is malformed.

That is the gap I wanted to solve with GenUIKit.

GenUIKit is an open-source TypeScript toolkit for turning LLM output into real UI components safely. It sits between the model and your frontend, validates the output against schemas, generates correction prompts when the output is wrong, and gives you a clean way to render trusted results in React.

This post walks through the problem, the mental model, and the pattern GenUIKit is built for.

The core idea

Instead of asking the model to generate arbitrary frontend code, you let it choose from components your app already owns.

That means the model can return something like this:

{
  "type": "WeatherCard",
  "props": {
    "city": "New York",
    "temperature": 18,
    "condition": "Cloudy"
  }
}
Enter fullscreen mode Exit fullscreen mode

That JSON is what I mean by UI-shaped JSON.

It is not raw HTML.
It is not JSX.
It is not executable code.

It is just structured data that says:

  • which component to render
  • which props to pass

The app still controls the real UI.
The model is only choosing from a predefined set of allowed components.

Why this gets messy fast

If you build this manually, the pattern usually looks like this:

if (output.type === 'WeatherCard') {
  // validate props
  // coerce values
  // render component
} else if (output.type === 'Table') {
  // validate props
  // handle edge cases
  // render component
} else if (output.type === 'Alert') {
  // validate props
  // render component
}
Enter fullscreen mode Exit fullscreen mode

That works in a demo.

Then the real problems start:

  • the model returns malformed JSON
  • the component name is unknown
  • the props are the wrong shape
  • the output is almost correct, but not quite
  • you end up writing ad-hoc retry prompts
  • validation logic gets duplicated across the server and client
  • the browser ships validation code it does not really need

The project ends up with a surprising amount of glue code for something that looked simple on day one.

What GenUIKit actually provides

GenUIKit

I do not think GenUIKit replaces your whole AI app.
It is much more specific than that.

It tries to own the boundary between LLM output and real UI rendering:

  • register allowed components
  • validate { type, props } against schemas
  • normalize defaults and reject invalid output
  • generate correction prompts automatically
  • support retry loops
  • render validated output in React
  • support streaming and action-oriented flows
  • support a server-validated lightweight client mode

What it does not replace:

  • your prompts
  • your business logic
  • your data fetching
  • your auth or persistence
  • your design system

It is infrastructure for model-driven UI, not a full app framework.

A simple example

Let’s say we want the model to choose whether to show a weather card.

import { z } from 'zod';
import { ComponentRegistry } from '@genuikit/core';
import { useGenerativeUI } from '@genuikit/react';

const weatherCardSchema = z.object({
  city: z.string(),
  temperature: z.number(),
  condition: z.enum(['Sunny', 'Cloudy', 'Rainy', 'Snowy']),
});

function WeatherCard({
  city,
  temperature,
  condition,
}: z.infer<typeof weatherCardSchema>) {
  return (
    <div>
      <h3>{city}</h3>
      <p>{temperature}°C</p>
      <p>{condition}</p>
    </div>
  );
}

const registry = new ComponentRegistry();
registry.register('WeatherCard', weatherCardSchema, WeatherCard);

function AssistantMessage({
  output,
}: {
  output: { type: string; props: Record<string, unknown> };
}) {
  const { element, ok, correctionPrompt } = useGenerativeUI(registry, output);

  if (!ok) {
    console.log(correctionPrompt);
    return <div>Trying again...</div>;
  }

  return <>{element}</>;
}
Enter fullscreen mode Exit fullscreen mode

If the model returns valid output, the component renders.

If the model returns invalid output, GenUIKit gives you a correction prompt you can send back to the model.

What happens when the model gets it wrong?

Imagine the model returns this:

{
  "type": "WeatherCard",
  "props": {
    "city": "New York",
    "temperature": "18",
    "condition": "Mostly cloudy"
  }
}
Enter fullscreen mode Exit fullscreen mode

That looks close, but it is still wrong.

  • temperature should be a number
  • condition should match the enum

GenUIKit catches that before render and can produce a correction prompt that says, in effect:

  • this component failed validation
  • here are the invalid fields
  • here is the expected shape
  • please send corrected props

That makes retries much more consistent than hand-writing one-off “please fix your JSON” prompts everywhere in the app.

The safer rendering model

One thing I try to be careful about is not overstating what “safe” means here.

GenUIKit does not mean:

  • “let the model generate arbitrary code”
  • “render any JSON and hope React survives”
  • “you no longer need normal frontend error handling”

What it does mean is:

  1. parse the model response
  2. reject malformed JSON before render
  3. verify that the component name is registered
  4. validate props against a schema
  5. only then hand the output to React

That sharply reduces the surface area of failure.

If one of your own registered components throws with valid props, that is still a normal app bug, and you should still use React error boundaries where appropriate.

So the claim is really safe contract boundary, not “magic immunity from UI bugs.”

Server-validated + lightweight client rendering

This is the part I’m most excited about right now.

A lot of apps do not need to ship schema validation to the browser at all.

If the server already validated the model output, the client can stay render-only.

Server:

import { ComponentRegistry } from '@genuikit/core';

const registry = new ComponentRegistry();
registry.register('WeatherCard', weatherCardSchema, WeatherCard);

const result = registry.validateOutput(modelOutput);

if (result.ok) {
  return result.output;
}
Enter fullscreen mode Exit fullscreen mode

Client:

import { ComponentRenderRegistry } from '@genuikit/core/client';
import { useValidatedUI } from '@genuikit/react/client';

const renderRegistry = new ComponentRenderRegistry();
renderRegistry.register('WeatherCard', WeatherCard);

function TrustedMessage({
  output,
}: {
  output: { type: string; props: Record<string, unknown> };
}) {
  const { element } = useValidatedUI(renderRegistry, output);
  return <>{element}</>;
}
Enter fullscreen mode Exit fullscreen mode

That gives you a cleaner separation:

  • server owns validation and retry logic
  • client only renders trusted payloads

In the real examples/chat-demo inside the GenUIKit monorepo, this reduced the browser bundle from about 78.4 KB gzip to 50.1 KB gzip.

There is a real demo app in the repo

I wanted this to be more than a toy snippet, so there is a full chat example in the repository:

  • examples/chat-demo
  • real chat UI
  • OpenAI / Anthropic support
  • LLM JSON -> validation -> correction prompt -> retry -> render
  • server-validated lightweight client rendering

There are also smaller examples for:

  • basic registry usage
  • streaming UI
  • bidirectional actions

When this pattern makes sense

I think this approach is useful when:

  • the model needs to choose what UI to show
  • you want structured, typed UI output instead of free-form text
  • you care about safe rendering boundaries
  • you want a repeatable retry/correction flow
  • you want to keep the browser light by validating on the server

I do not think you need this when:

  • your UI is fully deterministic
  • plain text responses are enough
  • you are not actually rendering model-driven components

Final thought

The interesting problem for AI UI is not “can the model output JSON?”

The interesting problem is:

Can we build a reliable contract between model output and real product UI?

That is the piece GenUIKit is focused on.

If you want to explore it:

If you try it, I’d especially love feedback on the server/client split and whether the correction-prompt model feels right in practice.

Top comments (0)