To keep the AI from breaking my design, it only writes JSON. I built that out for real, and the JSON turned into code
While mass-producing web tools with an AI, I've changed how I lock the design in three stages. The previous post I wrote about that got this comment: "I'd like to see the JSON approach and the design-system approach side by side." Taken at face value, I should just put the two side by side. But first, let me add a short preface. I don't want to frame this as "the JSON approach versus the design-system approach." When I called the JSON approach a "failure" in that post, I didn't mean the method is inferior; I meant it didn't suit my particular set of tools. A page made with the JSON approach does look thin. But where that thinness comes from is easily misread. Whether the design drifts and whether it looks rich are decided separately. What stops the drift is locking the design; whether it looks rich is how much you build out. What locking with JSON removes is drift in the items you specified in the schema. Whether the screen becomes rich, on the other hand, is determined by how much you've built out the machinery that turns that JSON into a screen. So it isn't that locking with JSON is what made it look like a spreadsheet. In the previous post, too, I wrote that fattening the schema and the renderer does increase the expression itself. But that came with a caveat: past a point, it heads toward rebuilding HTML and CSS by hand. What I really want to check is one step past that. If the template sets the ceiling on expression, then building out the JSON side's template as much as the current one should produce the same screen. So what does that build-out demand? I actually built it and measured. I'll share the result, along with the JSON-approach and design-system-approach screens placed side by side under matched test conditions. I'll admit up front: at the time, I chose the design system without running this comparison. So this is me building the road I didn't take, after the fact, and measuring what that cost consists of. Same order, same one-shot So that the comparison reflects the difference between the approaches, I matched everything I could match. I fixed the subject to a BMI calculator. On both sides, generation was a separate Claude Opus 4.8 session, and I handed over only "that approach's toolkit." One write only, no later edits, and to avoid contamination I let neither side read the production BMI calculator. The one thing I didn't match is the part that amounts to the definition of each approach itself. I told the JSON approach to "write one catalog entry," and the design-system approach to "write the page template." Match even that, and you're no longer comparing the two approaches. What came out in one shot, and what lies behind it Here's what I handed each side and what came out. JSON approach Design-system approach Toolkit handed over the entry schema (4 structure keys, plus 7 string keys the template reads)1 fixed template for BMI a build guidea pattern collectiona template spec1 reference template What the AI wrote 1 catalog entry (34 lines, fully conformant to the schema) 1 page template (313 lines) and a dictionary for 5 languages What decided the screen's contents the fixed template I handed over (the AI can't touch it) the template the AI wrote The same BMI calculator, generated one-shot from each era's toolkit alone. Input: 170 cm / 65 kg. The unit toggle, the disclaimer, and the four-band color coding weren't decided by the AI from nothing either. The parts, the requirements, and the color standard are held by the toolkit side (the build guide, the pattern collection, the template spec), and the AI assembled in line with them. Placed side by side, the JSON side looks thin. But this thinness isn't due to the approach. Build out that era's machinery as much as the current one and the same look comes out. The fact that the themes split light and dark isn't a difference of approach either. Theme wasn't a condition I matched for the test; it's the result of carrying over each era's actual setup as-is. The current design system's production theme is dark, and the AI used that layout and those tokens as-is. The JSON side is light simply because that era's template is light. Neither look nor theme divides the approaches here. Where the difference in approach shows is in how each one frays. The JSON side's fixed template hardcodes the WHO four-band labels in English on the JavaScript side, with no corresponding JSON key. So even if the AI writes the entry perfectly, the verdict alone comes out in English on the Japanese and Spanish versions. This isn't because JSON can't be localized; the key simply wasn't there. But adding that key is an extension of the schema and the renderer, and the AI can't reach it from within a one-shot entry. On the design-system side, the AI produced the four-band labels in all five languages on the spot. Prevention-type design locking isn't that flaws don't appear; it's a mechanism where you crush the flaws that do appear by widening the shared schema and renderer. The design-system side frayed too. Two of them, found in different ways. One bg-white, which is forbidden, had slipped into the class list of the scale marker. It violates the design system's rule of not writing a color like bg-white directly into a class, but letting the theme decide it. But on this screen the bg-white itself has no effect. Since this directive doesn't get into the built CSS, the stray bg-white never surfaces as white. So you can't tell it's a violation by looking; only a machine check that reads the class strings can catch it. The other is a different flaw, and this one you can tell at a glance. That marker looks like nothing more than a notch cutting darkly across the band, and the part that overshoots the band sinks into the background, making it hard to read where on the BMI it's pointing. There's no spec for how the marker should be shown, and no rule binding it. So the machine check passes it by, and this flaw stayed in the output as-is. In real operation, an inspection-and-touch-up step follows generation, and flaws like this get fixed there. This time the condition is one-shot only, without running that step, so this is how it came out. Inspection-type design locking isn't that flaws don't appear; it's a mechanism where you crush the flaws that do appear by adding to the pattern collection, the spec, and the hooks. Executing "just build out the template" Without breaking the JSON approach's rules, I built it out until it reached the same expressive elements as the current one-shot output. I set three rules not to break: the AI writes only the JSON catalog entry; it doesn't get to touch the template or the renderer it isn't allowed to write raw HTML, CSS, or JavaScript into that JSON the renderer doesn't get a BMI-specific branch At the level of expressive elements, I got there (the remaining compromises are listed at the end). The question is what swelled, and by how much, to get there. The real thing, extended while staying with the JSON approach until it reached the same expressive elements. Before extension After extension Catalog entry 34 lines, 24 values 343 lines, 258 values Schema vocabulary the AI must learn 11 words 113 words Renderer 433 lines 914 lines BMI-specific template 53 lines 1 line (just inherits the shared template) The extended entry's 343 lines ended up larger than the template body (313 lines) that the design-system approach wrote in one shot. The 113 words break down into 9 parts, 6 color tokens, 12 layout, 8 expression-DSL functions and 16 operators, 6 predicates, and 56 structure keys. It's already 433 lines before extension because a single renderer handles the drawing shared by all tools. The only thing that moved in the opposite direction is the BMI-specific template. At the start I wrote, "build out the template as much as the current one and the same screen should appear." The screen appeared, but the place you build out wasn't the template. Writing straight into the template is faster, but that means having hand-written HTML per tool, which is no longer the JSON approach but the design-system approach. If you bring it close to the current level while staying with the JSON approach, the place you build out becomes the renderer. In a design system, a tool's design is carried by its template. But the JSON approach's AI can write only data, and what raises design from that data is the renderer shared by all tools. So the design piles up in the renderer, and the template became empty. The AI can use the vocabulary that turns that design into a screen only as far as a human has defined it in the schema and implemented it in the renderer. So every time you add one form of expression, it's not just the entry the AI writes (34→343 lines) that grows; the schema vocabulary and renderer beneath it (11→113 words, 433→914 lines) grow just as much. Partway through, the entry's contents changed in character. What started as a catalog of text turned into a UI tree, style directives, and a formula sheet. The DOM, the template's conditionals, the CSS variables, the expression language, and the state management were all rebuilt as a dialect of JSON. The transformation showed in how it breaks, too. The calculation was originally written, fixed, in the template's JavaScript. The entry held only strings, and the AI doesn't touch the calculation. Where a tool-specific calculation goes was, in fact, decided by the approach itself. Add a dedicated branch to the shared renderer and you give up "a single sheet shared by all tools"; write directly into the template and you give up "the AI writes only data." Both head toward abandoning the JSON approach, so to keep the approach, the formula has nowhere to go but the data side. With the extension, that formula moved into the JSON, and it became the AI that writes the formula. A formula is a string, so misspell a variable name and there's no exception; it quietly becomes NaN. The calculation is top-to-bottom assignment, so get the order wrong and, likewise, it's quietly NaN. What I actually hit was floating-point evaluation order: 25*pow(h,2) and 25*h*h disagreed in the last digit. Keep fattening the schema and the renderer and you head, in the end, toward rebuilding HTML and CSS by hand. This prediction from the previous post appeared, for the first time, as a real artifact. Where the locking holds is on the values of colors and parts. The selectable colors stay at 6 tokens and the parts at 9 kinds, with no room for the AI to bring in arbitrary values. On the other hand, how the parts get assembled moved into the UI tree the AI writes. The values are locked, so no deviation appears there; but the AI decides the assembly, so that kind of flaw appears on the JSON side too. The marker we saw earlier on the design-system side is an example. What breaks quietly is the calculation; that, as seen earlier, is the layer the AI writes freely and that breaks without even raising an exception. And yet the 481 lines it added only cover the expression on a single BMI page. There isn't a single tab, table, chart, copy button, or file input/output in it. Every time you add another expression, you fatten the renderer that all tools share. As long as that renderer stays a single file, every new expression keeps piling onto that one file. Which to take: the JSON approach or the design-system approach So this isn't a comparison of an ugly approach and a pretty one. It's a comparison of two philosophies for how to guarantee consistency. The JSON approach is the prevention type. Because the colors and parts the AI can use are fixed, the drift of stray values sneaking in can't happen by construction. In return, everything you add in expression stacks up outside the AI. This time I funneled that into one shared renderer, so the more tools there are, the more that single file swells. This is where the prevention type is at its harshest. If many tools can share one type, the renderer only grows to the number of types. The more each tool looks different, the closer the buildup gets to the number of tools. The design-system approach is inspection-type. The AI assembles on its own, so it's cheap and fast, and because it can be written per tool, it keeps working even as the number of tools grows. In exchange, even the design's consistency is entrusted to a net of inspection and correction, and the guarantee goes no further than the net's precision. The bg-white from earlier was caught because there was a rule, but a flaw with no rule slips through. The cost of the inspection type doesn't show up in this one-shot comparison. That's because this time I deliberately didn't run the inspection process. In real operation, for each generated tool, you run several machine checks (forbidden classes, color scheme, multilingual-key consistency, string quality), check the screens for all five languages by eye, and fix until it clears a review gate. Fixes go up to three rounds at most, and if it doesn't pass there, that tool is shelved. Whereas prevention-type extension means "stack it into one renderer once and it's shared by all tools," this inspection-type process is an operating cost you pay one at a time, every time a tool is added. That there's a part only the net can guarantee is the same for both. What differs is the area that net covers. With the prevention type, since the colors and parts are already fixed, flaws that break the rules vanish before they reach the net. What remains for the net is only how the fixed parts get assembled, and the formulas. Inspection-type lets you freely write even the color values, so the net takes on rule-breaking flaws, design and all. And even prevention-type's net, like the NaN earlier from moving formulas to the AI, widens as you build out and edges toward the inspection type. Neither is superior; they just suit different situations. The prevention-type JSON approach suits situations where the tools are few, or many but all fitting one type, and you won't allow any drift at all. A rule-breaking look vanishes before it reaches the net, so there's no way for it to come out. The inspection-type design-system approach suits situations where the tools are many, each looks different, and you can run inspection and touch-up after generation. By having inspection stand in for consistency, you can ship each tool fast and cheap. In my case it was the latter. I have hundreds of web tools that each look different, and they can't be bundled by type. So at the time, before running this comparison, I steered toward the design system. The verification in this post is me walking that road I didn't take, after the fact, and turning what it costs into numbers. That said, this verification comes with things to discount. The way I matched conditions is thin on one side: to the design-system side I gave a pattern collection and a template spec to prevent violations, while for the JSON side I prepared no equivalent usage guide for the DSL vocabulary and no spec for the schema and renderer. If you truly matched the conditions you'd have to build these as well, so the real cost of bringing the JSON side up to the current level is even bigger than the line counts I measured. Also, this is a reproduction experiment generating a single BMI calculator one shot per side, not a general benchmark. Even the output I said got there keeps compromises, such as a 0.05 discrepancy in imperial units and a scale that isn't localized, and the numbers are specific to this one tool; a different tool would change them. Even so, at least when you're carrying hundreds of tools that each look different, the way I am, I think the prevention-type JSON approach costs more than the current design system. Caveats Verified: August 2026. Environment: generation on Claude Opus 4.8, screenshots on Playwright's headless Chromium (viewport 1280x900, 2× resolution, English page, input 170 cm / 65 kg giving BMI 22.5). Specs change, so the same steps won't necessarily produce the same numbers. The whole three-stage progression is written up in the previous post: The evolution of my method for locking an AI's design
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to