QA How-To
Testing structured output from an LLM (2026)
Learn testing structured output from an LLM with JSON Schema, Pydantic, current Responses API code, semantic checks, failure paths, and reliable CI gates.
18 min read | 2,764 words
TL;DR
Structured output is reliable only when it passes transport, syntax, schema, semantic, safety, and compatibility checks. Use a typed schema and current SDK parsing, validate business rules in code, cover failure branches, and split fast deterministic CI tests from focused provider contract tests.
Key Takeaways
- Separate transport, JSON syntax, schema, semantic, safety, and consumer compatibility assertions.
- Use provider-enforced Structured Outputs when available, then validate again at the application trust boundary.
- Keep cross-field rules, grounding checks, authorization, and high-risk decisions in deterministic code.
- Test null, missing, boundaries, adversarial inputs, refusals, incomplete responses, and transient failures explicitly.
- Version schemas and test every consumer because additive fields and enum changes can still break integrations.
- Run most contract tests without a model and keep a focused provider-backed suite for exact production schemas.
Testing structured output from an LLM means proving more than "the response parsed once." A production-quality test strategy verifies schema conformance, required fields, enums, types, cross-field business rules, refusal and incomplete paths, adversarial input, backward compatibility, and safe failure behavior.
This 2026 guide shows QA and SDET engineers how to test structured JSON produced by a language model. It uses the current OpenAI Responses API parsing helper with Pydantic, but the test design applies to any provider that supports JSON Schema or typed output. You will separate transport, syntax, schema, and semantic assertions so failures are precise and actionable.
TL;DR
| Layer | Question | Best oracle |
|---|---|---|
| Transport | Did the request complete normally? | SDK status and error handling |
| Syntax | Is the payload valid JSON? | JSON parser |
| Schema | Are types, enums, and required fields correct? | JSON Schema or Pydantic |
| Semantics | Do values agree with the input and business rules? | Deterministic domain assertions |
| Safety | Is refusal or unsafe content handled explicitly? | Branch and policy tests |
| Compatibility | Can consumers accept the new contract? | Consumer and version tests |
Use provider-enforced Structured Outputs when available, then validate again at the application boundary. A schema can guarantee shape, but it cannot guarantee that a category, amount, date, or summary is factually correct.
1. What Testing Structured Output From an LLM Must Prove
Testing structured output from an LLM covers four different failure classes. First, the model or transport can fail, time out, refuse, or return an incomplete response. Second, output can be syntactically invalid JSON. Third, valid JSON can violate the schema. Fourth, schema-valid data can be semantically wrong.
Consider this object:
{
"category": "billing",
"priority": "high",
"summary": "Customer requests a password reset",
"requires_human": false
}
It is valid JSON and may satisfy a schema, but the category conflicts with the summary. A parser-only test would pass a product defect. Conversely, a correct business classification wrapped in Markdown fences is invalid for a consumer that expects a raw object. Each layer needs its own assertion.
Define the contract before prompting. Specify property names, types, enums, nullability, required fields, additional-property policy, numeric boundaries, string constraints, date formats, and cross-field rules. Identify which requirements the provider can enforce and which the application must check.
Treat model output as untrusted input even when a provider advertises schema adherence. SDK changes, unsupported schema constructs, refusal paths, streaming assembly, proxies, and application transformations can still introduce failures. Validate at the consumer boundary and fail closed for actions with financial, security, or data-integrity impact.
For related prompt construction patterns, prompt engineering for test case generation shows how explicit constraints improve usefulness, but a prompt remains weaker than machine validation.
2. Compare JSON Prompting, JSON Mode, Structured Outputs, and Tool Calls
Teams often use "JSON output" to describe several different mechanisms. Their guarantees and test needs differ.
| Approach | Main guarantee | Best use | Key test risk |
|---|---|---|---|
| Prompt-only JSON | No protocol guarantee | Prototypes and provider-neutral fallback | Fences, prose, missing fields, wrong types |
| JSON mode | Valid JSON object | Flexible JSON with application validation | Shape can still violate the contract |
| Structured Outputs | Provider enforces supported schema | Typed extraction and response contracts | Semantic errors and unsupported schema features |
| Function or tool call | Model proposes typed arguments | Selecting and invoking application functions | Authorization, tool choice, and side effects |
Prompt-only JSON needs aggressive parsing and repair decisions. Avoid silently "fixing" arbitrary model output in high-risk flows because repair can change meaning. If repair is allowed, log the original, repaired form, reason, and validation result.
JSON mode guarantees syntactic JSON for supported APIs, not the exact fields and values. Structured Outputs uses a supplied schema and is the better fit when the provider supports the required subset. Tool calling is not merely formatting. The application must still authorize the selected function and validate arguments before execution.
Test the mechanism your production code actually uses. A local Pydantic unit test does not prove the provider accepted its generated schema. A live provider test does not prove every downstream consumer handles nulls and new enum values. Keep schema unit, provider contract, and consumer tests separate.
Do not assume all JSON Schema keywords are supported by every model or provider. Check current official documentation during implementation, keep the schema relatively simple, and add a smoke test that sends the exact production schema.
3. Design a Strict, Evolvable Schema
A good schema constrains ambiguity without encoding every business policy into provider syntax. Use descriptive property names and enums for closed sets. Require fields consumers always need. Reject additional properties when unknown data could hide mistakes. Make nullability explicit instead of treating missing, null, and empty string as interchangeable.
For a support-ticket classifier, a useful contract might contain:
- category: one of account, billing, bug, or other.
- priority: one of low, medium, or high.
- summary: a concise description grounded in the ticket.
- requires_human: an explicit boolean.
- customer_id: a string or null, but always present.
Define cross-field rules outside the provider schema. A high priority may require human review. A billing category may require a verified customer identifier before any account action. A summary must not introduce facts absent from the source. These are domain semantics, not merely types.
Version the contract. Additive optional fields can still break strict consumers, and enum additions can break exhaustive switches. A breaking rename should create a new schema version or a coordinated migration. Store the schema version beside the output and test old consumers against new producers where coexistence is expected.
Avoid overly broad types such as a free-form object for important data. Avoid one giant schema that combines unrelated tasks. Smaller task-specific schemas produce clearer prompts, simpler tests, and safer consumers. If the task cannot fit a stable schema, reconsider whether a typed output is the right interface.
4. Build a Runnable Typed Output Example
The current OpenAI Python SDK supports parsing a Responses API result into a Pydantic model. This runnable example uses a model name from an environment variable so the deployment can select an available Structured Outputs capable model.
# ticket_classifier.py
import os
from enum import Enum
from pydantic import BaseModel, ConfigDict
from openai import OpenAI
class Category(str, Enum):
account = "account"
billing = "billing"
bug = "bug"
other = "other"
class Priority(str, Enum):
low = "low"
medium = "medium"
high = "high"
class SupportTicket(BaseModel):
model_config = ConfigDict(extra="forbid")
category: Category
priority: Priority
summary: str
requires_human: bool
customer_id: str | None
client = OpenAI()
MODEL = os.getenv("OPENAI_MODEL", "gpt-5-mini")
def classify_ticket(text: str) -> SupportTicket:
response = client.responses.parse(
model=MODEL,
input=[
{
"role": "system",
"content": (
"Classify the support ticket. Use only facts in the ticket. "
"Set customer_id to null when it is not stated."
),
},
{"role": "user", "content": text},
],
text_format=SupportTicket,
)
parsed = response.output_parsed
if parsed is None:
raise RuntimeError(
f"No parsed ticket returned, response status: {response.status}"
)
return parsed
if __name__ == "__main__":
ticket = classify_ticket(
"Customer C-104 was charged twice and needs a person to review it."
)
print(ticket.model_dump_json(indent=2))
Install current packages and run:
python -m pip install -U openai pydantic
export OPENAI_API_KEY="your-api-key"
python ticket_classifier.py
The Pydantic model is both the requested format and the application validation boundary. extra="forbid" prevents unnoticed properties. The parsed result is a typed SupportTicket, not an unverified dictionary.
Keep provider calls behind a small adapter like classify_ticket. This makes failure policy, observability, retries, model selection, and tests consistent. Do not scatter direct SDK calls across business code.
5. Write Deterministic Schema and Semantic Tests
Most structured-output tests should not call a model. Test the schema and business rules with fixed objects, then add a small provider-backed contract suite. This split makes feedback fast and failure diagnosis clear.
# test_ticket_contract.py
import pytest
from pydantic import ValidationError
from ticket_classifier import Category, Priority, SupportTicket
def validate_business_rules(ticket: SupportTicket, source: str) -> None:
if not 1 <= len(ticket.summary) <= 240:
raise ValueError("Summary must contain 1 to 240 characters")
if ticket.priority is Priority.high and not ticket.requires_human:
raise ValueError("High-priority tickets require human review")
if ticket.customer_id and ticket.customer_id not in source:
raise ValueError("Customer ID is not grounded in source text")
def test_valid_ticket() -> None:
source = "Customer C-104 reports a duplicate charge."
ticket = SupportTicket.model_validate({
"category": "billing",
"priority": "high",
"summary": "Customer reports a duplicate charge.",
"requires_human": True,
"customer_id": "C-104",
})
validate_business_rules(ticket, source)
assert ticket.category is Category.billing
def test_unknown_property_is_rejected() -> None:
with pytest.raises(ValidationError):
SupportTicket.model_validate({
"category": "bug",
"priority": "low",
"summary": "Button label overlaps an icon.",
"requires_human": False,
"customer_id": None,
"internal_note": "must not be generated",
})
def test_invalid_enum_is_rejected() -> None:
with pytest.raises(ValidationError):
SupportTicket.model_validate({
"category": "urgent_billing",
"priority": "critical",
"summary": "Duplicate charge.",
"requires_human": True,
"customer_id": None,
})
def test_ungrounded_customer_id_is_rejected() -> None:
ticket = SupportTicket(
category=Category.account,
priority=Priority.medium,
summary="Login fails.",
requires_human=False,
customer_id="C-999",
)
with pytest.raises(ValueError, match="not grounded"):
validate_business_rules(ticket, "User cannot sign in.")
These tests cover shape and domain rules deterministically. Add cases for missing required fields, explicit null, empty and maximum-length strings, booleans represented as strings, case-sensitive enum values, Unicode, and serialization round trips.
A provider integration test should send a small set of stable inputs and validate the returned type plus a few unambiguous semantics. Do not exact-match a generated summary. Check that the category is correct, required identifiers are grounded, and business validation passes.
6. Use Boundary, Adversarial, and Metamorphic Inputs
Structured output changes how a model formats a response, not whether the input is safe or easy. Build cases that challenge both the schema and the task.
Boundary cases include empty input, whitespace, one-character text, maximum accepted application length, large Unicode text, line breaks, repeated content, code, tables, dates, decimal values, and every enum category. Test null versus omitted data explicitly. If a field is nullable but required, ensure the consumer handles null and does not assume the property disappears.
Adversarial inputs may ask the model to add an unapproved property, return XML, wrap JSON in a fence, change a field type, reveal the schema, or place instructions inside a field. Provider-enforced structure should preserve the shape, while semantic and security rules still decide whether values are acceptable.
Metamorphic tests compare related inputs:
- Adding harmless punctuation should not change the ticket category.
- Reordering unrelated sentences should not invent a customer ID.
- Replacing a stated ID should update that field and no unrelated field.
- Removing an ID should produce null.
- A paraphrase should preserve the category and priority when meaning is unchanged.
- Appending an injection request should not add unknown fields or expose protected context.
Do not demand identical summaries for paraphrases. Assert invariants that express the product contract. Save unexpected outputs as regression candidates after human review. For adversarial security depth, see testing prompt injection vulnerabilities.
7. Handle Refusals, Incomplete Responses, and Transport Failures
Happy-path schema tests are not enough. The application must define behavior when there is no parsed object. Causes can include safety refusal, request validation error, provider timeout, rate limit, network failure, cancellation, or incomplete generation.
The adapter should classify failures into stable application errors. Do not catch every exception and return an empty object, because downstream code may treat empty values as a valid low-risk result. Fail closed when the output controls an action. A customer-support UI may ask the user to retry, while a payment workflow may route the case to human review.
Test at least these paths with mocks or a provider test environment:
- The SDK returns a parsed object.
- The response completes without a parsed object.
- The provider rejects the schema.
- The request times out.
- The provider rate-limits the request.
- The response is cancelled or incomplete.
- Application validation rejects a semantic rule.
- Logging redacts the original sensitive input.
Retries should be bounded and limited to transient failures. Retrying a schema or business-rule defect without changing anything can increase cost and hide the problem. Attach request IDs and candidate configuration to telemetry, but do not log confidential prompts or model output by default.
If streaming structured output, do not expose partial JSON as a completed business object. Assemble according to the SDK's supported streaming interface, wait for the final response, then validate. Consumers should observe an explicit loading or failed state instead of a half-built object.
8. Validate Semantics, Grounding, and Business Rules
Schema conformance prevents structural ambiguity, but semantic testing determines whether the object is useful. Design field-level oracles from the source input and trusted business data.
For extraction, verify that identifiers, amounts, dates, and quoted names appear in or are deterministically derived from the source. Normalize formats before comparing, but preserve evidence. For classification, create reviewed examples for every class, boundaries between classes, multi-label ambiguity, and an other or abstain path where appropriate.
Cross-field assertions are powerful. Examples include:
- priority=high requires requires_human=true.
- A refund action requires a verified customer ID.
- An end date cannot precede a start date.
- A percentage must be between the domain limits.
- A summary must not contain credentials or unsupported claims.
- A tool action must match authenticated server context, not a model-generated user ID.
Calculate field-level accuracy or confusion matrices for labeled datasets instead of one object-level pass rate. An object may have four correct fields and one dangerous wrong field. Weight or gate fields by risk, but keep raw per-field results visible.
For free-text fields, deterministic grounding checks, required terms, and prohibited claims should come before model-based judges. If you use a judge for summary fidelity, define the rubric, calibrate against human labels, and keep it separate from schema validation.
9. Add Contract, Consumer, and Regression Coverage
A structured-output producer is part of a distributed contract. Test the services, queues, databases, front ends, and analytics jobs that consume its object. A field accepted by Pydantic may fail a database constraint or an exhaustive TypeScript switch.
Keep representative contract fixtures under version control. Serialize them with the producer and deserialize them with each consumer. Include minimum and maximum valid strings, nulls, every enum value, Unicode, and future-compatible fields according to the version policy. If additional properties are forbidden, ensure both producer and consumer agree.
When changing a schema:
- Classify the change as compatible or breaking for each consumer.
- Update the version and migration plan.
- Run old-producer to new-consumer and new-producer to old-consumer tests where coexistence occurs.
- Re-run the model-backed benchmark because schema wording can change model behavior.
- Monitor parse and semantic failure rates after deployment.
- Preserve rollback compatibility.
Store regression examples for failures such as a wrong enum, invented identifier, null mishandling, refusal crash, and unexpected property. Avoid adding only the exact wording that failed. Add nearby variants that represent the underlying class.
Generating API tests from OpenAPI with AI offers complementary ideas for schema-driven consumer tests, but generated tests still require reviewed boundary and semantic assertions.
10. Automate Testing Structured Output From an LLM in CI
Testing structured output from an LLM in CI should use a test pyramid. Run pure schema, business-rule, serialization, and consumer tests on every commit. Run a compact provider contract suite when the schema, prompt, model configuration, or SDK changes. Run a larger labeled semantic benchmark on a schedule or before release.
A provider test can be marked explicitly:
# test_ticket_integration.py
import os
import pytest
from ticket_classifier import Category, classify_ticket
@pytest.mark.integration
@pytest.mark.skipif(
not os.getenv("OPENAI_API_KEY"),
reason="OPENAI_API_KEY is required for provider contract tests",
)
def test_billing_ticket_contract() -> None:
source = "Customer C-104 says the card was charged twice."
ticket = classify_ticket(source)
assert ticket.category is Category.billing
assert ticket.customer_id == "C-104"
assert ticket.summary.strip()
Run deterministic tests on every pull request and opt into the integration test in the secured job:
pytest -m "not integration" -q
pytest -m integration -q
Record schema hash, prompt revision, model, SDK version, and labeled-dataset version. Fail immediately on parse, schema, grounding, or high-risk business-rule errors. For probabilistic classification quality, compare a versioned candidate with an approved baseline and inspect per-class changes.
The CI report should distinguish transport failures, refusals, parse failures, schema violations, semantic failures, and consumer failures. This taxonomy shortens diagnosis and prevents a generic "invalid JSON" bucket from hiding the real risk.
Interview Questions and Answers
Q: What is the difference between valid JSON and valid structured output?
Valid JSON only satisfies syntax. Structured output must also satisfy the expected schema, including required fields, types, enums, and additional-property rules. Production correctness further requires semantic and business validation.
Q: Does Structured Outputs eliminate validation code?
No. Provider schema enforcement reduces formatting failures, but the application must validate at its trust boundary and enforce domain rules. It also needs explicit handling for refusals, incomplete responses, transport errors, and consumer compatibility.
Q: How would you test a nullable required field?
I test a valid concrete value and explicit null, and I verify that omission is rejected if the field is required. I also test serialization and every consumer because some languages or databases collapse missing and null differently.
Q: Why should semantic tests be separate from schema tests?
A schema-valid object can contain the wrong category, invented ID, or impossible date relationship. Separate tests identify whether the failure is structural or factual. That makes release rules and debugging clearer.
Q: How do you avoid flaky exact assertions on LLM output?
I exact-match deterministic fields only when the input has an unambiguous expected value. For generated summaries, I assert length, grounding, required facts, and prohibited claims rather than identical wording. I compare model-backed quality over a reviewed dataset.
Q: How would you evolve a structured-output contract?
I version the schema, analyze every consumer, and test compatibility in both directions during mixed deployment. I treat enum additions and optional fields as potentially breaking. I rerun provider and semantic benchmarks because schema changes can alter output behavior.
Common Mistakes
- Stopping at JSON parsing: Add schema, semantic, safety, and consumer checks.
- Trusting provider enforcement blindly: Validate at the application boundary and test failure branches.
- Encoding business logic only in the prompt: Enforce cross-field and authorization rules in code.
- Silently repairing malformed output: Repair can change meaning. Define and audit any allowed repair policy.
- Exact-matching generated prose: Assert grounded invariants rather than one wording.
- Treating missing, null, and empty string as equal: Specify and test each state.
- Ignoring refusals and incomplete results: A parsed happy path is only one branch.
- Adding enum values without consumer tests: Exhaustive switches and database constraints may break.
- Running every test against a paid model: Keep most contract logic deterministic and use a focused provider suite.
Conclusion
Testing structured output from an LLM is contract testing plus semantic evaluation. Use a strict, evolvable schema, parse through a supported SDK path, validate again in application code, and test refusals, boundaries, grounding, business rules, and consumers independently.
Start with one typed model and a dozen deterministic boundary cases, then add a small provider contract suite and a reviewed semantic benchmark. This layered approach turns structured output into a dependable interface rather than JSON-shaped hope.
Interview Questions and Answers
What is the difference between valid JSON and valid structured output?
Valid JSON satisfies only the grammar. Valid structured output also matches required properties, types, enums, nullability, and additional-property rules. I then apply semantic and business validation because a schema-valid value can still be wrong.
Does provider-enforced Structured Outputs eliminate application validation?
No. It reduces formatting failures but does not guarantee business truth, authorization, or every transport path. I validate again at the application boundary and handle refusals, incomplete responses, and provider errors explicitly.
How would you test a nullable required field?
I verify a valid concrete value and explicit null both pass. I verify omission fails when the field is required and that empty string follows its own documented rule. I also run serialization and consumer tests because platforms handle null and missing differently.
Why separate schema and semantic tests?
A schema-valid object can contain an invented customer ID, incorrect category, or impossible date order. Separate layers identify the real defect and support different release rules. Structural failures are deterministic, while semantic quality often needs a labeled dataset.
How do you avoid flaky assertions on generated text fields?
I exact-match only unambiguous deterministic values. For summaries, I check length, source grounding, required facts, prohibited claims, and domain rules. I evaluate broader quality across reviewed cases rather than expecting one wording.
How would you evolve an LLM output schema safely?
I version the schema, classify compatibility for every consumer, and test old and new combinations during rollout. I treat enum additions and optional properties as potentially breaking. I also rerun the provider benchmark because schema changes can alter model behavior.
Frequently Asked Questions
What should I test in LLM structured output?
Test request completion, refusal and incomplete paths, JSON syntax, schema conformance, required fields, enums, types, nullability, additional properties, semantic grounding, cross-field business rules, and consumer compatibility. Keep each failure category visible.
Is valid JSON the same as Structured Outputs?
No. Valid JSON only guarantees syntax. Structured Outputs aims to match a supplied schema, while production correctness also requires that values are grounded, internally consistent, authorized, and useful to downstream consumers.
Do I still need Pydantic validation if the provider enforces a schema?
Yes. Validate at the application trust boundary so SDK, transport, transformation, and unsupported-path defects fail safely. Pydantic also provides a clear typed contract for business code and deterministic tests.
How do I test LLM JSON without calling a model every time?
Create fixed valid and invalid objects and test schema, serialization, domain rules, and consumers directly. Reserve a small integration suite for the provider, exact production schema, and a few unambiguous semantic cases.
How should an application handle an LLM refusal with structured output?
Treat absence of a parsed object as an explicit non-success branch. Map it to a stable application error or human-review outcome according to risk, and never replace it silently with an empty object that consumers may accept.
Can adding an optional field break structured-output consumers?
Yes. Strict deserializers, database mappings, snapshots, and exhaustive code can reject an additive field. Analyze compatibility for each consumer, version the contract when needed, and test mixed-version deployment paths.