A language model returns text that is usually the shape you asked for. A mobile interface needs data that is always the shape it can render. That gap — between a probabilistic response and a deterministic UI — is where most AI features quietly break in production.
The fix is not a better prompt. It is treating the model’s output as a contract: an explicit, versioned, validated interface between an unpredictable producer and a UI that must never crash, hang, or show a blank screen. This article describes how I design that contract for consumer AI apps, and why the discipline matters more than the model choice.
It is tempting to start from the prompt and see “what the model gives back.” That produces a feature whose data shape is discovered at runtime, on a user’s device, in a locale you did not test.
Instead I start from the UI’s needs and write the schema first: the exact fields the screen must have, their types, which are optional, and what a minimum renderable result looks like. The prompt is then written to satisfy the schema — not the other way around.
For example, in an AI image-generation feature the UI does not need the raw model payload — it needs a caption, an image reference, a style descriptor, and a status. That is the schema. Everything else the model returns is noise the screen should never depend on.
A useful test: if you cannot draw the screen from the schema alone, the schema is not finished, and no amount of prompt tuning will save it.
The single most important rule: the model’s response is untrusted input until it has passed validation. Parse it against the schema at the server boundary (or a controlled client layer), before any UI code touches it.
Validation should answer three questions:
Anything that fails becomes a typed failure, not an exception that bubbles into the render path. A feature that shows a controlled “couldn’t generate that — try again” is reliable; one that renders half-parsed JSON is not.
“The model failed” is not a diagnosis. In practice, structured-output failures fall into a small, stable set that the product should own explicitly:
invalid_structure — did not parse or missing required fieldswrong_types — parsed but fields have unexpected typesout_of_range — values violate business rules or enumsempty_result — valid but contains nothing renderablepolicy_refusal — the model declinedtruncated — cut off before completionunclassified — none of the above (keep this visible; do not force it into a neighbour)Naming these lets the interface respond correctly to each and lets the team see, over time, which failure is growing — which is usually the difference between a prompt regression, a provider change, and a UI bug.
The category that is easiest to miss is empty_result: a technically valid response that contains nothing the UI can render. Nothing “failed”, so it slips past naive error handling — but to the user the feature simply did nothing. Giving it a name is what makes it visible.
Output depends on the prompt, the schema, the model, and the app release. If any of these changes without a version tag, you lose the ability to explain a regression.
I record stable identifiers with every generation — schema_version, prompt_version, model_route, app_version, locale — as configuration identifiers, never as copies of the content. When quality drops after prompt_version=12 in one locale, that is a testable hypothesis and a rollback target, not a mystery.
This also protects evaluation: a retention or “regenerate” metric only means something when you know which contract the user actually experienced.
A robust AI feature degrades in defined steps rather than failing in one jump:
request_id for support.The key is that every rung produces a valid object for the UI. The interface should never need to know how far down the ladder the system went — only that what it received satisfies the contract.
In my own work the reliability of these features comes less from any single clever fallback and more from the discipline around them: testing the contract before release with unit and UX tests, and monitoring the feature in production with crash and error tracking so that when something does slip through, it surfaces immediately and gets fixed fast.
An output contract deserves behavioural tests, written before the feature ships:
invalid_structure and no unparsed text reaches the UI.out_of_range and the fallback ladder is entered.truncated → repaired.Tests should also assert absence: internal instructions, provider credentials, and any field the client must never see must never appear in the validated object.
{
"schema_version": "s3",
"prompt_version": "p12",
"model_route": "quality-default",
"app_version": "2.4.1",
"locale": "en-GB",
"result": {
"title": "string (1–80 chars)",
"items": "array<{ id: string, label: string }> (0–20)",
"confidence": "enum: high | medium | low"
},
"outcome": "valid_structure",
"fallback_used": false
}
The exact fields vary per feature. The principle does not: define what the UI needs, validate everything the model returns against it, name the failures, version the configuration, and degrade in defined steps.
It is easy to believe that shipping AI features is about choosing the right model. In a consumer product, reliability comes from the boundary you build around it: a schema the UI can trust, validation that treats model output as untrusted, named failure modes, versioned configuration, and a fallback ladder that always yields a renderable result.
Models will keep changing. A well-designed output contract is what lets your feature keep working while they do — and what lets your team explain, test, and improve it instead of hoping.