LLM Output Validation
LLM output validation is the code that checks a language model’s response before the application uses it, together with the tests that prove those checks work. When output feeds software rather than only a human reader, it needs a contract: usually JSON that must match a JSON Schema, plus business rules and policy checks. A response that breaks the contract is rejected, retried or replaced with a safe fallback before it reaches a database, a screen or another system.
Example: a store’s support inbox asks a model to triage each customer message and return intent, priority, order_id and a draft reply. The backend routes the ticket by intent and shows reply to a support agent. Validation checks that the response is JSON, that intent is one of four allowed values, that the order number really appears in the message, and that the reply is displayed as text, never as markup.
Why it matters
The lesson on hallucination testing asked whether an answer is true to its sources. This lesson asks whether the application can safely use it. A missing field crashes a parser, "Refund" instead of "refund" sends a ticket to the wrong queue, an invented order number triggers an action on someone else’s order, and a script hidden in a reply runs in an agent’s browser. These are ordinary defects from an unusual source.
For the tester, a contract turns vague output into something you can assert on. A schema check is deterministic even when the wording is not, so it can run on every build. And because a model will sometimes produce bad output however good the prompt is, the useful question is not "does the model always comply?" but "what does the application do when it does not?" — and that you can test.
Structured output: give the response a contract
Structured output means the model answers in a machine-readable format, almost always JSON, with fixed field names and allowed values. A line in the prompt such as "reply only in JSON" is a request, not a guarantee. Many model APIs also offer a JSON mode, a structured-output option or tool calling, which constrain the response, to different degrees, to valid JSON or to a schema you supply. Read what your provider actually guarantees: it may cover syntax only, or only part of JSON Schema, and a response can still be cut off at the output token limit or be a refusal instead of data.
Write the contract as a JSON Schema kept in the repository and used by both the application’s validator and the tests. In the schema below, additionalProperties: false rejects fields nobody asked for, enum fixes the allowed values (case-sensitive), and pattern constrains the order number. order_id is required but may be null: a message without an order number gets an explicit null, not a missing field or an invented number. The guide on JSON explains how schema validation goes beyond checking that the text is valid JSON. Note that format values such as date are annotations by default in recent drafts, so many validators ignore them unless configured; use pattern or a code check when the format matters.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["intent", "priority", "order_id", "reply"],
"additionalProperties": false,
"properties": {
"intent": { "enum": ["refund", "delivery", "account", "other"] },
"priority": { "enum": ["high", "normal", "low"] },
"order_id": { "type": ["string", "null"], "pattern": "^ORD-[0-9]{6}$" },
"reply": { "type": "string", "minLength": 1, "maxLength": 800 }
}
}Validate in layers: parse, schema, rules, guardrails
Each layer catches what the one before it cannot. A response that fails any layer never reaches the consumer as it is: it is logged with the prompt and model versions, and the application retries within a hard limit or falls back to something safe, such as a human queue. Test that failure path as carefully as the happy path: what the user sees, that the retry limit holds, and that the log is enough to reproduce the case.
- Parse: is it JSON at all? Prose before the JSON, a Markdown code fence or a truncated object fails here. If the application repairs output, for example by stripping a fence, that repair is specified behaviour to test.
- Schema: required fields, types, allowed values, lengths, no extra fields. A schema failure makes the output unusable, however good the wording is.
- Business rules: do the values make sense for this input and this data? The
order_idmust appear in the message and belong to that customer. This layer catches invented values that are perfectly schema-valid. - Guardrails: policy checks around the model that block, redact or replace output — personal data in a reply, a forbidden topic, a link outside an allow-list. Test both directions: bad output is stopped, and normal output is not blocked by mistake.
- 1Model responseRaw text from the model: untrusted input
{"intent":"refund",…} - 2ParseIs it JSON at all?
Branch: Prose, a code fence or cut-off JSON: rejectFail
- 3Schema checkRequired fields, types,
enumvalues, no extra fieldsBranch: Missing field or a value outside the
enum: rejectFail - 4Business rulesValues fit this input: the order number is in the message
Branch: Invented or impossible value: reject and logFail
- 5GuardrailsPolicy: personal data, forbidden topics, link allow-list
Branch: Blocked, redacted or replaced, and loggedCaution
Gate: Passed every layer
- 6Accept, still untrustedEach consumer encodes for its context: HTML, SQL parameters, allow-listsPass
- On any rejectionLog it with the prompt and model versions; retry within a hard limit or fall back to a human queue
Consistency across runs
The same input can give different output on different calls. Lowering the temperature reduces variation but does not guarantee identical responses, so one passing run proves little. Run each important case several times, for example 10, with the production model version, prompt version and parameters, and record N and every result.
Do not compare whole responses as strings. Decide which fields must be stable — intent, priority, extracted IDs and amounts — and which may vary, such as the wording of reply. A decision that flips between runs is a defect even when each answer looks reasonable, because the same request gets different treatment. Consistent output can still be consistently wrong, so consistency checks complement correctness checks; they never replace them.
- Schema validity: every run should pass. At production volume, one failure in 10 calls means many failed requests every day.
- Decision agreement: count the distinct values each stable field took across the runs.
- When a decision flips, ask whether the rule is defined at all. Often the fix is to state the rule in the prompt, or to compute that field in ordinary code.
Treat model output as untrusted input
The OWASP Top 10 for LLM Applications 2025 lists improper output handling (LLM05) as a risk of its own: model output passed to other components without sufficient validation, sanitisation and handling. Whoever wrote the input, or the documents the model read, can influence what it writes, so receiving code must never trust output because "it came from our own model".
Schema validity is not safety: "reply": "<b>Sorry</b>" is a valid string. Each consumer handles output for its own context: encode it, or use a strict sanitiser, before showing it as HTML; pass it into SQL as a parameter, never by concatenation; check URLs, file names and commands against an allow-list; and never let the model trigger an action the user may not perform. Test with harmless markup and metacharacters, only on systems you own or have written permission to test. The guide on LLM security testing lists benign cases for each context.
Try it: Special Character Dataset Generator Try it: HTML Entity Encoder
Worked example: one triage case, ten runs
Input: "Hi, order ORD-204913 arrived broken. I want my money back." The tester ran this case 10 times against the schema above and recorded, for each distinct response, the first layer it failed, if any. Five patterns appeared (below). R4 is the instructive one: its order number matches the pattern but has two digits swapped, so only the business rule catches it.
In all 10 runs intent was refund, but priority was normal 7 times and high 3 times. The bug report states the input, N = 10, the split, the model and prompt versions, and one question for the product owner: are damaged goods high priority? Once the rule is agreed, either state it in the prompt with examples, or have the model extract only the fact (did the item arrive damaged?) and compute priority from it in ordinary code, which an exact test can check.
R1 {"intent":"refund","priority":"normal","order_id":"ORD-204913","reply":"..."}
parse ok, schema ok, rules ok -> accept
R2 Here is the JSON: {"intent":"refund", ...}
parse fails: text before the JSON -> reject, one retry
R3 {"intent":"refund","priority":"normal","reply":"..."}
schema fails: required field order_id is missing -> reject, one retry
R4 {..., "order_id":"ORD-204931", ...}
rules fail: order number is not in the message -> reject, human queue
R5 {..., "reply":"<b>Sorry</b>, the item arrived broken..."}
all layers pass -> accept, show as textCommon mistakes
- Trusting "reply only in JSON" in the prompt, or a provider’s JSON mode, and skipping validation in code.
- Treating a schema-valid response as a correct one: the schema checks shape, so an invented order number that matches the pattern still passes.
- Running a case once, or comparing whole responses as strings, instead of repeating runs and checking the fields that must be stable.
- Testing only that a guardrail blocks bad output, never that normal output gets through and never what the user sees after a rejection.
- Rendering, storing or executing output without encoding for its context because it came from the team’s own model.
The QA perspective
Start in refinement or design review: ask for the output contract, who owns it, which fields must be stable, and what happens on each kind of rejection. If nobody can answer, that is your first finding. Review every schema change like an API contract change, because the prompt, the backend and the tests all depend on it.
Automate the deterministic layers on every pull request with a fixed test set, and run the repeated-run consistency suite nightly or before a release, with thresholds agreed in advance. After release, ask for rejection counts by layer from production logs: a rising schema failure rate after a model or prompt change is a regression signal. Involve security in reviewing every consumer of the output, and the product owner in every business rule the model applies.
Practice exercise
Validate receipt extraction output
An expense app sends the text of a receipt to a model and saves the JSON it returns; merchant is shown in an HTML expense list. The contract: merchant is a string of 1 to 80 characters, date is written YYYY-MM-DD, total is a number greater than 0, currency is SAR, USD or EUR, category is travel, meals, office or other, and no other fields are allowed. Business rule: total equals the TOTAL line of the receipt.
For responses A to G below, name the first layer that fails (parse, schema or business rule) or write "accept", and say what the app should do. Then: the same receipt, run 10 times, returned category meals 6 times and other 4 times. What do you report, and what do you suggest?
Receipt: AL NOOR CAFE, Riyadh. Date: 2026-09-14. 2 x Coffee @ 18.00. TOTAL: SAR 36.00
A {"merchant":"Al Noor Cafe","date":"2026-09-14","total":36.00,
"currency":"SAR","category":"meals"}
B Here is the data: {"merchant":"Al Noor Cafe", ...}
C as A, but "date":"14/09/2026"
D as A, but "total":"36.00"
E as A, but "total":18.00
F as A, but "merchant":"<i>Al Noor</i> Cafe"
G as A, plus "tip":0- Read the contract and the business rule.
- Check each response layer by layer and stop at the first failure.
- For accepted responses, decide how the expense list must display the values.
- Write the consistency finding as a short bug report.
Show the model answer
A: accept. B: parse fails, there is text before the JSON; reject and retry once, or strip the prefix only if that repair is specified. C: schema fails, the date does not match YYYY-MM-DD. D: schema fails, total is a string, not a number. G: schema fails, tip is a field the contract does not allow. For C, D and G, reject and retry once within the limit; if the retry also fails, send the receipt to manual entry.
E: passes parse and schema but fails the business rule: 18.00 is the price of one coffee, not the receipt TOTAL of 36.00. Reject it, log it and send the receipt to manual review; only a rule check catches a plausible, schema-valid value like this.
F: accept, because markup in a merchant name is still a valid string. The expense list must display it as text, showing <i>Al Noor</i> Cafe literally or removing the tags with an allow-listed sanitiser, never rendering it as HTML.
Consistency: report the receipt, N = 10, the split (meals 6, other 4), and the model, prompt version and parameters. The same receipt is filed in different categories on different runs. Ask the product owner to define the category rule (for example, cafes and restaurants are meals), put it into the prompt with examples or into code, and agree a consistency threshold for the test set before release.
Frequently asked questions
If my provider guarantees schema-conformant output, do I still need to validate?
Yes. Read the guarantee closely: it may cover only part of JSON Schema, a response can still be cut off or be a refusal, and models and providers change. Business rules and guardrails are yours in any case. Validation in your own code is cheap, and its tests keep working when the provider changes.
What is the difference between validation and a guardrail?
Validation checks the output against your contract and business rules and answers "can the application use this?". A guardrail enforces a policy, such as no personal data or no off-topic advice, and may block, redact or replace the output. Both run in code around the model, and both need tests that show they stop bad output and let normal output through.
Should the application retry automatically when validation fails?
A bounded retry is a common design for parse and schema failures, sometimes with the validation error added to the new request. It needs a hard limit, because every retry costs time and money. Business-rule and policy failures usually go to a fallback, such as a human queue or a safe message, rather than being retried until the model complies.
References
- OWASP Top 10 for LLM Applications 2025The ten most critical security risks of LLM applications (LLM01:2025–LLM10:2025), starting with prompt injection and sensitive information disclosure.
- JSON Schema SpecificationThe vocabulary for describing and validating the shape of JSON documents (types, required properties, formats).
- OWASP Cheat Sheet Series: Cross Site Scripting PreventionPreventing XSS: output encoding for each context, sanitizing user-authored HTML, safe DOM sinks and framework protections, with CSP only as defence in depth.
- Define success criteria and build evaluationsA model provider's guidance on writing measurable success criteria and evaluation sets graded by code, by exact match or by a model.
Practise with Mutqan tools
Validate a JSON document against a JSON Schema (draft-07 / 2019-09 / 2020-12 core keywords).
Validate JSON syntax and get precise error locations with helpful fix suggestions.
Compare two JSON documents semantically and highlight added, removed and changed values.
Test regular expressions live with match highlighting, capture groups and replace preview.
Generate inputs full of special characters: quotes, SQL/HTML/JSON metacharacters, control chars, emoji and path tricks.
Escape HTML special characters (<, >, &, quotes) into entities to prevent XSS.
Go deeper
- LLM Security Testing: Prompt Injection and MoreImproper output handling: treat model output as untrusted input
- LLM Testing: How to Test LLM ApplicationsHow to assert on non-deterministic output
- LLM Evaluation: How to Evaluate LLM OutputsStep 5: run, repeat and set thresholds
- What is JSON?How to validate JSON
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.