Building AI Features in React

Asking for JSON Your Components Can Render


The first version of the compare feature asked for "a comparison of these two products as a table". The model returned Markdown tables. One run had 3 columns, the next had 9. One cell contained a pipe character, which split it into two columns. Rendering it meant pulling in a Markdown library, and even then the team could not highlight the better product in each row, make the table sortable, or give it proper table headers for screen readers.

A Markdown table is a presentation. A component wants data, which it then presents itself. This lesson designs the compare feature's JSON shape from the CompareTable component's props and writes the prompt that produces it.

The same comparison, as presentation and as dataMarkdown table• 3 columns one run, 9 the next• A pipe in a cell splits the column• No way to mark the better product• About 520 output tokensJSON rows• Same columns every time• An enum verdict drives the style• Evidence as review ids to link• About 340 output tokens
Ask for data and let the component present it — the smaller shape also arrives about three seconds sooner.

Design the JSON from the props

Start with what the component needs to render, not with what the model can produce. Here are the props of the table you will build in section 3:

TypeScript
// What CompareTable needs to render one rowtype Verdict = "a" | "b" | "tie" | "unclear";type CompareRow = {  aspect: string;     // row header, e.g. "Battery"  a: string;          // what buyers say about product A  b: string;          // what buyers say about product B  better: Verdict;    // drives the highlight style  evidence: string[]; // review ids that support this row};

Each design choice here has a reason:

  • Flat rows. An array of simple objects maps straight onto <tr> elements. Nesting makes the model more likely to get the structure wrong.
  • Enums for anything that drives style or logic. better picks a CSS class and an accessible label, so it must be one of four known values. "Product A is somewhat better" in a string cannot drive a style.
  • Ids, not text, for links. evidence holds review ids that the UI turns into links. The UI already has the review text; it only needs to know which ones.
  • An explicit "unclear". Without it, the model must pick a winner even when reviews disagree. The option gives it an honest answer to choose.
  • Nothing that code already knows. Product names, prices and average stars come from your database. Asking the model for them costs tokens and gives it a chance to get them wrong.

Put the shape in the prompt

The prompt shows one complete example object and states each rule in plain words. Models follow a concrete example far more reliably than a description alone.

TypeScript
// server/prompts.ts (continued)export const COMPARE_PROMPT_VERSION = "compare-v2";export const COMPARE_SYSTEM = `You compare two products using only their buyer reviews.Reply with a single JSON object and nothing else, in exactly this shape:{"rows":[{"aspect":"Battery","a":"...","b":"...","better":"a","evidence":["r1042","r2217"]}]}Rules:- 3 to 6 rows. Each aspect is 1 to 3 words, such as "Battery" or "Comfort".- "a" describes product A and "b" describes product B, each under 90 characters.- "better" is one of "a", "b", "tie" or "unclear". Use "unclear" when reviews disagree or are few.- "evidence" lists 1 to 4 review ids that support the row, taken only from the input.- If aspects are given, use exactly those aspects, in that order.`;export function compareUserPrompt(  a: { name: string; reviews: Review[] },  b: { name: string; reviews: Review[] },  aspects?: string[],): string {  const wanted = aspects?.length ? `Aspects: ${aspects.join(", ")}\n\n` : "";  return `${wanted}Product A: ${a.name}\n${formatReviews(a.reviews)}\n\n` +    `Product B: ${b.name}\n${formatReviews(b.reviews)}`;}

Notice the 90-character rule. At 360 pixels wide, each product column is about 140 pixels, which fits about 22 characters per line. A 90-character cell is four lines, the most the row can hold before it looks broken. That number came from the layout, not from a guess.

Let the provider constrain the output, when it can

Most major providers now offer a structured-output mode. You pass a JSON Schema, and the model is constrained during generation so that its reply parses and matches the schema's structure. This removes most shape failures: no code fences, no missing braces, no extra prose.

It is worth using when your provider supports it, but it does not remove validation, for three reasons:

  • Meaning is not checked. A schema can say evidence is an array of strings. It cannot say those strings must be ids from this request.
  • Not every rule is supported. Some providers ignore or reject schema keywords like maximum string length or array length.
  • Replies can still end early. A token limit, a refusal or a network error can leave you with something that is not the object you wanted.

ShopLens keeps its llm wrapper text-in, text-out so it works with any provider, and puts the shape in the prompt. The Zod check in the next lessons catches the rest. If you switch on a provider's structured mode later, the validation code does not change; it just fails less often.

Small shapes are faster shapes

Every output token takes time to generate. In the compare shape, one row is about 60 tokens, so six rows is about 360 tokens. At 60 tokens per second, that is 6 seconds of generation. Twelve rows would be 12 seconds, and a shopper will not wait that long for a table.

This is why the prompt caps rows at six and cells at 90 characters. It is also why the JSON uses short, readable keys like a and b rather than productADescriptionFromReviews: every key is repeated in every row and costs tokens each time. Do not go to the other extreme with keys like x1; the model follows meaningful keys more reliably, and your teammates can read the logs.

Check your understanding

0 of 3 answered

1.Why is better an enum of "a", "b", "tie" and "unclear" rather than a free-text string?

2.You turn on your provider's structured-output mode with a JSON Schema. What validation do you still need?

3.The compare prompt asks the model to include each product's name and price in the JSON. What is wrong with that?