To test LLM JSON output, give each test case a JSON schema and fail the build when the model's response doesn't parse or doesn't validate. Drift-CI's json-schema evaluator does exactly that on every pull request: invalid JSON or a schema violation scores 0, and a drop from a passing baseline fails the check.
Why structured output breaks
Classifiers, extractors and tool-calling prompts all hand their output to code. That code wants a specific shape: an object, these keys, these enum values, a number between 0 and 1. The model doesn't know that unless the prompt says so, and it keeps following the prompt only as long as nothing changes.
Plenty does change. A prompt edit adds a field or reorders instructions. A model swap brings a model that likes to wrap JSON in a markdown fence or add "Here is the classification:" first. A new enum value appears that your code has never seen. Each of these turns into a parse error or a wrong branch in production, usually on a fraction of inputs, which is why spot-checking misses it.
Test LLM JSON output with the json-schema evaluator
Add json-schema to a case's evaluators and put the schema next to it. This case is from our dogfood suite, which tests a support-ticket triage prompt:
- id: classify-structured-billing
tags: [classification, json-schema]
systemPrompt: |
Classify the support ticket. Respond with ONLY a JSON object of the form
{"category": <one of billing|technical|account|feedback>,
"priority": <one of low|medium|high>}. No prose, no code fences.
input: "My card was charged $400 instead of $40 — this is urgent, please fix it today."
evaluators: [json-schema]
schema:
type: object
required: [category, priority]
properties:
category:
type: string
enum: [billing, technical, account, feedback]
priority:
type: string
enum: [low, medium, high]
additionalProperties: falseThe evaluator parses the raw response and validates it with Ajv. It's binary:
- the response isn't valid JSON: score 0, reason "Output is not valid JSON";
- it parses but breaks the schema: score 0, with every violation listed (for example a missing required property or a value outside the enum);
- it validates: score 1.
Two details matter. First, the schema is per case, so a suite can cover several output shapes. Second, a case that lists json-schema without a schema is rejected when the suite loads, so you can't ship a check that silently checks nothing.
additionalProperties: false is worth keeping. It catches the model adding fields you didn't ask for, which is often the first sign that it's reading the instructions differently.
Combining a schema with exact-match
A schema tells you the shape is right, not that the answer is. To pin the answer as well, add exact-match to the same case's evaluator list and set expected to the exact response you want. The case score is then a weighted combination of the two; with no weights given, each evaluator counts equally.
Be precise about what exact-match compares. It checks the whole response against expected, after trimming whitespace and lowercasing both. It doesn't compare individual fields, so a response with different spacing or key order won't match even if the values are identical. That works well for short, tightly specified outputs. For anything looser, keep the schema check and use an LLM judge or a rubric for the content.
The Drift-CI repo's classification example shows several schemas for one intent classifier, including an array field with minItems.
Evaluator errors versus regressions
Drift-CI draws a line between "the output was bad" and "we couldn't score the output":
- Invalid or non-conforming JSON is a score, and a low one. If the baseline scored 1 and this run scores 0, the case is a regression and the check fails.
- An evaluator error is a case Drift-CI couldn't score at all: for example an LLM judge that failed, or a request the provider rejected. Those cases get no score, show an orange marker in the PR comment, and are excluded from delta math, so they never count as regressions.
- Transient provider failures are never regressions either; the guide's section on flakiness and outages covers them.
This matters for structured output in particular. When a model switch starts wrapping JSON in a code fence, you want a red check, not a vague error. In our own dogfood runs, trying a cheaper model for the triage agent produced exactly that kind of red check: both structured classification cases dropped from 1 to 0 against their baselines and failed the PR, while the plain-label classification cases passed.
Running it in CI
Run the suite on every pull request with the Drift-CI GitHub Action; the GitHub Actions tutorial walks through the workflow line by line.
Once a case passes, accept its baseline and commit it. From then on, any prompt or model change that breaks the shape shows up in review. For the wider workflow around baselines and thresholds, see prompt regression testing. If you're about to switch models, read testing an LLM model upgrade first; structured cases are where our model swaps broke. The LLM regression testing guide covers how schema checks fit alongside judges and similarity scoring.