Create model output as a JSON Schema contract
In this article, we summarize how to use Structured Outputs to handle LLM responses as JSON Schema-based contracts.
The first question you encounter when attaching an LLM response to a service is not “Is the answer plausible?” “Can the backend parse it?”
If your model answers in free text, your frontend, DB, and subsequent APIs won't be able to use it reliably. In particular, tasks that require structured results, such as document summarization, information extraction, classification, routing, and tool call preprocessing, require model outputs to be contracts.
Analysis base date: 2026-05-12 Practice standard environment: OpenAI API, Python, Pydantic, FastAPI Main reference materials: OpenAI Structured Outputs Docs, JSON Schema, Pydantic
Key takeaways
- Structured Outputs is a function that makes model responses follow the specified JSON Schema.
- LLM responses must be treated as contracts like API responses within the service.
- Without a schema version, it is difficult to track changes in prompts and response structures.
- Validation failure should be viewed as a normal operating scenario and a retry/fallback should be designed.
- Instead of leaving all fields to the model, the fields to be calculated by the server should be filled in by the server.
1. Why Structured Outputs?
Free text responses are great for humans to read, but difficult for systems to process.
For example, let's say you made the following request to the model:
# This is an example.Summarize the main content of this document and include sources and credibility.
The model can answer in many different ways.
# This is an example.The key point is that Redis cache reduces DB load. The source is the Redis official document.
or:
# This is an example.{"summary": "Redis cache reduces DB load","source": "Redis Docs"}
Services hate this volatility. The frontend expects an array ofcitations, but if the model gives it the stringsource, it breaks.
2. Difference between JSON mode and Structured Outputs
JSON mode helps you generate valid JSON. However, this is different from necessarily adhering to a specific schema.
Structured Outputs focuses on ensuring that the response follows the JSON Schema defined by the developer.
| division | JSON mode | Structured Outputs |
|---|---|---|
| target | JSON format output | JSON Schema compliance |
| Ensure required fields | weakness | strong |
| enum constraints | weakness | schema based |
| API contract compliance | limited | height |
In practice, schema-based output is safer than JSON mode.
3. Response schema design principles
A good schema doesn't leave everything to the model.
| principle | explanation |
|---|---|
| Leave only the fields you need | Excessive fields increase the failure rate |
| Actively use enums | Ensure stability in subsequent quarters |
| Exclude server calculated fields | Token usage, trace_id, etc. are filled in by the server. |
| Use nullable carefully | Nullability increases subsequent processing complexity |
| put version | Schema change tracking |
4. Document Q&A response schema example
// This is an example JSON structure.{ "type": "object", "properties": { "answer": { "type": "string", "description": "Answers to user questions" }, "confidence": { "type": "string", "enum": ["high", "medium", "low"] }, "citations": { "type": "array", "items": { "type": "object", "properties": { "document_id": { "type": "string" }, "chunk_id": { "type": "string" }, "quote": { "type": "string" } }, "required": ["document_id", "chunk_id", "quote"] } }, "needs_human_review": { "type": "boolean" } }, "required": ["answer", "confidence", "citations", "needs_human_review"], "additionalProperties": false}
This schema clearly separates answers, their credibility, source, and whether they require review.
5. Validation and fallback
Even when using Structured Outputs, backend validation is required. Output from external systems should always be verified.
# This is example code.from pydantic import BaseModelfrom typing import Literal # This declaration shows an example flow.class Citation(BaseModel): document_id: str chunk_id: str quote: str # This declaration shows an example flow.class AnswerOutput(BaseModel): answer: str confidence: Literal["high", "medium", "low"] citations: list[Citation] needs_human_review: bool
Failure Scenario:
# This is an example.1. Model response schema validation failed2. Retry once3. If it still fails, return a fallback response4. Save failure cases in eval/debug dataset
Fallback example:
// This is an example JSON structure.{ "answer": "Could not reliably generate answers. Please be a little more specific with your question.", "confidence": "low", "citations": [], "needs_human_review": true}
6. Schema versioning
Response structures change over time. Therefore, schema version is required.
# This is an example.answer.v1answer.v2classification.v1extraction.receipt.v1
The schema version must be recorded along with the prompt version.
| record entry | reason |
|---|---|
schema_version | Response structure tracking |
prompt_version | Track prompt changes |
model | Quality comparison by model |
validation_result | Failure rate analysis |
7. Separate DB storage and API response
There is no need to make the model output schema and API response schema exactly the same.
Model output:
// This is an example JSON structure.{ "answer": "...", "confidence": "high", "citations": []}
API response:
// This is an example JSON structure.{ "answer_id": "ans_123", "answer": "...", "confidence": "high", "citations": [], "usage": { "input_tokens": 1000, "output_tokens": 300 }, "trace_id": "0af..."}
usage,trace_id, andanswer_idare values added by the server.
8. Practice risks
| risk | react |
|---|---|
| Schema is too complex | Start with a small schema |
| enum does not contain reality | unknown or needs_review design |
| Model fabricates origin | Citations are limited to selection only by search result chunk_id |
| Client crashes when changing schema | versioning and migration |
| View validation failure only as a failure | Collected as normal operating indicators |
9. Practical checklist
# This is an example.[ ] Did you distinguish between model output schema and API response schema?[ ] Are the required fields clear?[ ] Have you restricted additionalProperties?[ ] Are enum values connected to subsequent logic?[ ] Do you save schema_version?[ ] Is the validation failure rate recorded as a metric?[ ] Is there a retry/fallback policy in case of failure?[ ] Did you prevent the model from randomly generating citations?
Failure case: JSON received but no contract
Using Structured Outputs does not immediately create a stable API contract. The most common failure is when you create a schema but don't have a version, validation, or fallback policy. The model returns JSON, which the parser also passes, but the meaning may not be what the downstream service expects. For example, if it is not documented what thresholdrisk: "medium"means, the UI, DB, and alert rule interpret it differently.
Another problem is leaving all the fields to the model. Some values, such ascreatedAt,sourceId,confidenceScore, andneedsReview, must be calculated by the server. If you trust the timestamp or internal ID filled in by the model, data consistency will be broken. Structured Outputs is a tool for controlling output format, not a replacement for business rule validation.
Example implementation: schema version and server calculated fields
type ExtractionV1 = { schemaVersion: "customer_intent.v1"; intent: "refund" | "upgrade" | "bug_report" | "unknown"; evidence: string[]; modelConfidence: number;}; type StoredExtraction = ExtractionV1 & { requestId: string; validatedAt: string; needsReview: boolean;};
The model only producesExtractionV1. The server calculatesrequestId,validatedAt,needsReview. In particular,needsReviewmust not only look at model confidence, but also look at server information such as user rating, sensitive keywords, and previous failure history.
Comparison table: output formats and operating agreements
| item | What Structured Outputs can help you with | What the server must continue to do |
|---|---|---|
| JSON format | Generate response matching schema | Handling parsing failures and retries |
| enum | Allowed value limits | Define the business meaning of the value |
| required field | Reduce missing fields | Recalculate Untrusted Values |
| schema | structural fixation | Compatibility with version migration |
Based on this table, Structured Outputs are at the front of the validation layer. The backend still needs to validate external input and check for business invariants before saving. This ensures that model replacement or prompt changes do not directly lead to downstream failures.
Practical application example: storing inquiry classification results
Let's say you want to create a function that classifies customer inquiries into intents such as refund, billing, bug, and account. It is best to keep the model output schema small.
{ "schemaVersion": "ticket_intent.v1", "intent": "billing", "evidence": ["invoice number", "charged twice"], "confidence": "medium"}
Here, the server separately addsrequestId,receivedAt,customerId, andneedsHumanReview. In particular,needsHumanReviewis not determined by model confidence alone. Sensitive intents such as refunds, personal information, and payment failures can be sent for review even if the confidence level is high.
A failure is to make your schema too large too quickly. If you receivedepartment,assignee,refundAmount,legalRisk, andreplyDraftat once from the beginning, it is easy to mix up the business meaning even if the model format matches. Separating the classification schema and the answer draft schema reduces the scope for failure. Even if the classification is incorrect, answer generation may not start, and even if the answer draft fails, a classification log may be left.
The comparison criteria are simple. What Structured Outputs guarantees is that “the response is close to the schema shape.” What the service must guarantee is that “the value matches the current user, permissions, DB state, and policy.” Separating the two prevents you from running the wrong subsequent API just because you received JSON.
10. Q&A
Q1. If I use Structured Outputs, can I avoid using validation code?
no. Validation still needs to be done in the backend. It is fundamental not to trust external system results.
Q2. Should all LLM responses be JSON?
no. The final answer you show your users might be Markdown. However, it is a good idea to structure the values that the system must subsequently process.
Q3. What if my schema gets too big?
It's better to break it down into smaller pieces. Separate the extraction, classification, response, and review needs into step-by-step schemas rather than putting them into one large schema.
11. References and uncertainty
References
- OpenAI Structured Outputs:https://platform.openai.com/docs/guides/structured-outputs
- JSON Schema:https://json-schema.org/
- Pydantic:https://docs.pydantic.dev/
uncertainty
- Supported models and detailed API parameters may change over time.
- Performance and failure rates of complex schemas require real-world testing.

댓글
GitHub 계정으로 로그인하면 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions를 통해 운영됩니다.