On this page
- There are three different problems
- Prompted JSON
- JSON mode
- Schema-constrained output
- Structure and truth are different guarantees
- Design the schema for the decision you need
- Required fields force explicit decisions
- Do not use free-form strings when an enum is the real domain
- Schema support differs by provider and model
- Validate again in your application
- Business validation belongs outside the schema
- Structured output is not tool calling
- You can combine tools and structured final output
- Streaming structured output is different from streaming text
- Incremental parsers are possible, but use them for the right reason
- Truncation needs explicit handling
- Refusals and safety outcomes need a separate path
- Retry only when the failure is repairable
- Repair prompts are weaker than native constraints
- Schema complexity has a cost
- Version your contracts
- Do not let the model generate trusted IDs unnecessarily
- Provenance matters for extraction
- Measure structural and semantic failure separately
- Test adversarial inputs
- A practical implementation checklist
- Where BYOKchat fits
- Further reading
Sometimes you do not want an AI model to write prose. You want data your program can consume:
{
"priority": "high",
"category": "billing",
"needs_human": true
}
The difficult part is not asking for JSON. The difficult part is getting output that matches a contract reliably enough to enter application code.
Modern APIs increasingly support schema-constrained structured output, which is stronger than prompting “return JSON” and stronger than older JSON-only modes. But schema adherence still does not make the values semantically correct or safe.
There are three different problems
Do not collapse these into one feature.
Prompted JSON
You tell the model:
Return a JSON object with category and priority.
The model may comply, but nothing at the API layer necessarily guarantees valid JSON or the exact shape.
JSON mode
The API constrains the output to syntactically valid JSON, but the object may not match your intended schema.
For example, valid JSON can still be wrong:
{
"importance": 9000
}
when your application expected:
{
"priority": "high"
}
Schema-constrained output
The API uses a supplied schema to constrain the generated structure. OpenAI’s current Responses API exposes this through text.format with type: "json_schema", and recommends JSON Schema Structured Outputs over the older json_object mode for supported models.
This greatly improves structural reliability.
It does not prove that "priority":"high" is the correct classification.
Structure and truth are different guarantees
Suppose the schema is:
{
"type": "object",
"properties": {
"total": { "type": "number" },
"currency": { "type": "string" }
},
"required": ["total", "currency"],
"additionalProperties": false
}
The model may return perfectly valid structure:
{
"total": 1250,
"currency": "USD"
}
while the source invoice actually says $1,250 CAD.
Schema constraints solve serialization. They do not eliminate extraction errors, hallucination, ambiguous source data, or business-rule mistakes.
Design the schema for the decision you need
Avoid giant “future proof” objects containing dozens of optional fields.
If the task is routing a support ticket, the application might need only:
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "other"]
},
"urgency": {
"type": "string",
"enum": ["low", "normal", "high"]
},
"requires_human": { "type": "boolean" }
},
"required": ["category", "urgency", "requires_human"],
"additionalProperties": false
}
Narrow enums are often more useful than open-ended strings when the downstream application has a fixed state machine.
Required fields force explicit decisions
Optional fields can hide uncertainty.
Instead of leaving email optional, where absence could mean “not found,” “not applicable,” or “model forgot,” model the state explicitly when it matters:
{
"type": "object",
"properties": {
"email": {
"type": ["string", "null"]
},
"email_status": {
"type": "string",
"enum": ["found", "not_found", "ambiguous"]
}
},
"required": ["email", "email_status"],
"additionalProperties": false
}
The exact JSON Schema subset accepted by an AI provider can be narrower than the full specification, so verify nullable/union syntax and other keywords against that provider. The modeling principle is general: make meaningful uncertainty part of the contract.
Do not use free-form strings when an enum is the real domain
This:
"priority": "pretty urgent"
is difficult for application code.
If valid states are known, encode them:
"enum": ["low", "normal", "high"]
Then downstream code can switch exhaustively and tests can cover every value.
Schema support differs by provider and model
Do not assume every API accepts the entire JSON Schema specification or the same subset.
Provider differences can include:
- supported schema keywords;
- nesting limits;
- enum limits;
- recursion/reference support;
- optional/nullable representation;
- strictness controls;
- compatible models;
- streaming behavior.
A multi-provider client should represent its application requirement separately from the provider-native request and reject/degrade deliberately when a target cannot satisfy it.
Validate again in your application
Even when the provider promises schema adherence, validate at the application boundary.
Why?
- provider/model capabilities change;
- proxies and compatible endpoints may behave differently;
- responses can be truncated or fail;
- your business rules can be stricter than JSON Schema;
- bugs happen.
A safe pipeline is:
Only the final domain object should enter sensitive application logic.
Business validation belongs outside the schema
A schema can say:
amount is a positive number
It may not know:
amount must not exceed the authenticated account's remaining budget
Similarly, a schema can validate a file path string but should not be your only defense against directory traversal.
Separate:
shape validity
semantic validity
authorization
Structured output is not tool calling
This distinction matters.
Use structured output when the answer itself is data:
classify ticket
extract invoice fields
produce UI configuration
return search filters
Use tool calling when the model needs the application to perform an operation:
query CRM
send email
read file
create issue
A tool’s arguments are structured data too, but they participate in an execution loop.
See How AI Tool Calling Works.
You can combine tools and structured final output
A workflow might:
- search internal records using tools;
- retrieve supporting evidence;
- return a final structured decision.
For example:
{
"decision": "approve",
"risk_level": "low",
"evidence_ids": ["doc_12", "doc_91"]
}
The application can then render or process the result deterministically.
Streaming structured output is different from streaming text
A partial JSON document is usually invalid:
{"category":"bill
You can render raw fragments for debugging, but application logic should not consume them as a completed object.
Some provider APIs expose typed streaming events and final structured content. Treat the provider’s completion boundary and response status as authoritative, then parse/validate the complete object.
Do not trigger a business action because the first half of a stream happens to contain a parseable prefix.
Incremental parsers are possible, but use them for the right reason
For very large structured streams, an incremental JSON parser can expose completed subtrees before the full document finishes.
That can improve UI responsiveness, but it creates more states:
field unseen
field partially received
field structurally complete
response complete
response failed/incomplete
If the entire object is small, waiting for completion is simpler and safer.
Truncation needs explicit handling
A response can stop before completing the intended generation because of output limits, cancellation, provider errors, or network failure.
Do not treat:
HTTP 200 + parseable JSON
as the only success condition.
Check provider completion status/finish reason where available. A truncated response may occasionally form valid JSON while still being semantically incomplete.
For critical workflows, require both:
provider says generation completed
AND
application validation succeeds
Refusals and safety outcomes need a separate path
A model may refuse a request rather than produce the requested schema. Your application should not force every possible outcome into the business schema if the provider protocol represents refusal separately.
Model execution state can include concepts such as:
completed structured result
refused
incomplete
failed
cancelled
Treat these as control states, not malformed business objects.
Retry only when the failure is repairable
If parsing/schema validation fails unexpectedly, a bounded retry can sometimes help.
But classify the cause:
| Failure | Good next step |
|---|---|
| Provider/model lacks schema support | Choose compatible path/model |
| Output truncated | Increase/rebudget output or simplify schema |
| Schema too complex | Simplify schema |
| Business rule failed | Re-prompt with relevant constraint or escalate |
| Safety refusal | Do not retry merely to evade safeguards |
| Network failure before result | Retry according to network policy |
Blindly sending “try again” five times is not a recovery strategy.
Repair prompts are weaker than native constraints
Before native structured-output features, applications often used:
Your previous output was invalid JSON. Fix it and output only JSON.
This remains useful when a provider lacks stronger constraints, but it costs another generation and still relies on probabilistic compliance.
Prefer native schema constraints where supported, then retain validation as defense in depth.
Schema complexity has a cost
A schema is part of the generation contract. Extremely large schemas can:
- increase request size;
- consume context;
- hurt prompt-cache stability if frequently changed;
- increase provider compilation/processing overhead;
- make failures harder to diagnose.
Keep schemas focused. If the domain object contains 200 fields but the model only needs to produce 12, expose those 12.
Version your contracts
Structured output often feeds durable application data. That makes schema evolution important.
Store a schema/version identifier when persistence matters:
{
"schema_version": 2,
"category": "billing",
"urgency": "high"
}
Or version outside the model-generated payload if you do not want the model controlling it.
When the schema changes, old persisted generations should remain interpretable.
Do not let the model generate trusted IDs unnecessarily
If the application already knows the authenticated user ID, tenant ID, or target workspace, do not ask the model to repeat it and then trust the repeated value.
Bind trusted context in application code:
model output: { category, urgency }
application adds: authenticatedTenantID
This reduces both errors and attack surface.
Provenance matters for extraction
For document extraction, consider asking for evidence locations alongside values when the provider/schema supports your design:
{
"invoice_total": 1250,
"evidence": {
"page": 2,
"quote": "$1,250.00"
}
}
Then the application can verify or show the source to a human.
Do not confuse provenance with proof—the model can misidentify evidence too—but it improves auditability.
Measure structural and semantic failure separately
Useful metrics include:
parse failure rate
schema validation failure rate
provider incomplete/refusal rate
business validation failure rate
retry rate
human correction rate
A 0% JSON parsing failure rate can coexist with poor extraction accuracy. Measuring only syntax hides the problem users care about.
Test adversarial inputs
A structured-output implementation should be tested with:
- empty source text;
- ambiguous values;
- conflicting documents;
- very long input;
- Unicode;
- prompt injection inside source data;
- output truncation;
- unsupported schema keywords;
- provider/model switch;
- enum edge cases;
- nullable fields;
- network failure during streaming;
- refusal/safety outcomes.
The test oracle should validate domain semantics where possible, not merely JSON.parse() success.
A practical implementation checklist
Before relying on structured AI output:
- distinguish prompted JSON, JSON mode, and schema-constrained output;
- use narrow schemas with explicit enums and required fields;
- know the target provider’s supported schema subset;
- validate output again locally;
- apply business rules after schema validation;
- keep trusted identity/authorization outside model output;
- check provider completion state before accepting streamed output;
- model refusal/incomplete/failure separately;
- use bounded, classified retries;
- version durable contracts;
- measure semantic errors separately from syntax errors.
Where BYOKchat fits
For a multi-provider client, structured-output capability belongs in the model/provider capability profile. The application can keep a provider-neutral intent—“this request requires schema-constrained JSON”—while adapters map it to native controls only where the selected provider supports them.
That is safer than assuming every OpenAI-compatible endpoint implements every structured-output feature identically.