Keep the Prompt and the Validator in One Room
A shared schema stops drift between services. It does not stop the prompt and the schema drifting from each other, because the prompt is prose and the schema is a list — and prose is not checked by anything.
So the test suite checks it:
it('builds a prompt that declares every schema section for the category', function (string $category) {
$prompt = schemaSyncPrompt($category);
foreach (ContentSchema::sections($category) as $section) {
expect($prompt)->toContain('"'.$section.'":');
}
})->with(['business', 'event']);
it('exposes exactly the schema section union in the SiteContent resource', function () {
$resource = new SiteContentResource(SiteContentDTOMapper::toDTO([]));
$keys = array_keys($resource->toArray(request()));
$union = ContentSchema::allSections();
sort($keys);
sort($union);
expect($keys)->toBe($union);
});
Neither test calls a model. Neither costs a token. They run in milliseconds and they are among the most valuable tests in the codebase, because they catch the specific mistake that is otherwise invisible: adding a section to the schema, forgetting to ask the model for it, and shipping a feature that requests one shape and validates another.
If you take one testing idea from this book before Chapter 12, take this one. Assert that the thing you ask for and the thing you accept are the same thing.
When Validation Fails
You now have a response that has failed a required-path check. What happens next depends entirely on whether the work is repeatable.
If nothing has been written yet, retry — this is genuinely one of the cases where trying again works, because the failure is non-deterministic. Cap it at two or three attempts, and log the failure rather than swallowing it: a required path that fails regularly is a prompt problem, and you will only find out if the failures are visible.
If something has been written, you have a much harder problem, and no amount of validation will save you. That is a job for Chapter 10.
The instinct to avoid is patching. Do not fill in a missing hero.title with the business name and carry on. It feels helpful. It converts a loud, countable failure into a silent one, and you lose the signal that told you the prompt needs work. If the response is not usable, say so.
Key Principles
- Turn on structured output, and know its limits. It guarantees syntax. It guarantees nothing about keys, and nothing about truth.
- Strip the fence, then decode or fail. Never partially rescue a malformed response.
- Fail into the shape the caller expects. A parse failure that returns a message in the right structure keeps one code path; one that returns null creates two.
- Describe the structure once, in a framework-free class every service can read. Three descriptions of one contract will drift, and the drift is invisible.
- Version the schema. Generated content outlives the prompt that made it.
- Required means "throw it away and retry". Reserve it for paths whose absence corrupts something. One good required path beats forty defensive ones.
- Any checkable external fact will eventually be invented. Supply it, or instruct the model to defer — and always give it a correct alternative move.
- Test that the prompt asks for what the validator accepts. No tokens, no model, milliseconds, and it catches the one mistake nothing else can see.