Engineering
Schema-Derived Prompting: Why We Generate LLM Output Contracts From Code, Not Prose
A real production bug — and the structural fix (z.toJSONSchema in the prompt, not a hand-written description) that eliminated a whole class of model-swap failures.
October 23, 2026 · 6 min read · Gentle Pills Engineering
Every AI task in Gentle Pills asks a language model to return structured JSON — a list of candidate symptoms, a set of rubric suggestions, a remedy comparison. The model's output gets parsed and validated against a Zod schema before anything is persisted. For most of this system's life, the *prompt* that described that expected shape to the model was written separately from the schema that validated it — two sources of truth for the same contract, maintained by hand, and that gap eventually produced a real production bug worth describing in detail, because the fix changed how every task in the system is prompted.
The bug
The suggest_rubrics task takes a case's structured symptoms and asks the model to propose matching rubrics from the loaded repertory corpus. Its original prompt ended with an instruction along these lines:
Respond with JSON matching this shape exactly as described by the field names above.That's prose. It relies on the model having correctly inferred, from the surrounding context, which exact field names and nesting the response needs — in this case, a top-level suggestions array of objects. On gpt-4.1, this worked. Not because the instruction was unambiguous, but because that particular model's training distribution happened to converge on the same shape the schema expected, often enough that the mismatch never surfaced.
It broke the moment suggest_rubrics was routed to gpt-5.4-mini as part of moving the task onto tiered model routing. The model came back with a plausible-looking, well-formed, completely wrong JSON object: {"rubrics": [...]} instead of {"suggestions": [...]}. Schema validation rejected it, the task failed, and the failure mode was genuinely confusing at first — the model wasn't malforming JSON, it was malforming the *contract*. It had guessed a reasonable field name for "a list of rubric suggestions" and guessed wrong, because the prompt never actually told it the real one.
Why this isn't a one-off fix
The tempting fix is local: add rubrics as an accepted alias, or hand-edit that one prompt to spell out suggestions explicitly. Both patch the symptom. Neither addresses the mechanism — a prose description of a JSON shape is an informal contract, and informal contracts drift differently across model families and even across versions of the same family. Any other task whose prompt described its output shape in words rather than stating it precisely had the identical latent bug, just not yet triggered. Fixing only suggest_rubrics would have meant waiting for the next model swap to find the next one in production.
The actual fix: generate the JSON Schema directly from each task's own Zod schema at prompt-build time, via z.toJSONSchema(), and embed that literal schema in the prompt instead of describing it in prose.
const jsonSchema = JSON.stringify(z.toJSONSchema(schema));
Respond with JSON matching exactly this JSON Schema — every property
name must match precisely, no extra top-level properties:
{jsonSchema}This was applied to every task's prompt in the system, not just the one that had already broken. The reasoning: the underlying failure mode — a model guessing a plausible-but-wrong shape when given only a prose description — doesn't care which task triggered it first. It's a property of the pattern, and the pattern was used everywhere.
What changes when the schema is the source of truth
Once the prompt is derived from the schema rather than written alongside it, the two can't drift apart. There's no longer a separate English description of the contract to fall out of sync when a field gets renamed or a new optional field gets added — the Zod schema *is* the contract, used for both the model-facing instruction and the runtime validation that rejects malformed output. A field rename shows up automatically in the next prompt build; there's no second place to remember to update.
This also changes what a model swap actually tests. Before this change, moving a task to a different model tier meant hoping the new model's guessing heuristics happened to line up with the old model's. After it, a model swap tests something much narrower and more tractable: can this model follow an explicit JSON Schema instruction at all — a capability that's close to universal across modern instruction-tuned models — rather than can this model happen to guess the same informal shape as the previous one. The failure surface shrinks from "anything about how this model infers structure from prose" to "whether this model can follow an explicit schema," which is both easier to reason about and easier to test for before a tier gets reassigned.
The general principle
The specific bug here was a field-name mismatch, but the broader lesson generalizes past this one system: if an LLM-facing prompt and a runtime validator are both describing the same output contract, write that contract once and derive both from it. A prose description of a JSON shape is not documentation of a contract — it's an informal restatement that happens to usually work until it doesn't, and the point at which it stops working is almost always silent until something downstream (a parser, a schema validator, a doctor staring at a failed AI suggestion) surfaces it. Treating the schema as the single source of truth, and generating every consumer of that contract — prompt included — from it, removes an entire category of bug that otherwise only shows up when you change a detail you had no reason to think was load-bearing.
See it running, not just described
Try two of these AI tools yourself with no sign-up, or register a free clinic workspace — it's free for the first 50 clinics.
Reminder: this information is published for qualified practitioners. Patients must not self-medicate — homeopathic prescribing depends on the complete individual symptom picture, which only a doctor can evaluate. Please visit a homeopathy doctor.