How to get JSON from OpenAI API: Structured Outputs
If a program is reading the AI response, phrases like "return only JSON" are not enough. Structured Outputs gives the model a formal contract: which fields, types, and allowed values the result must contain.

openai and a basic understanding of JavaScript objects. A ChatGPT subscription does not replace separate API payments.Problem: valid text is not yet reliable data
Imagine a support form. A user writes: "After paying for order 4821, the money was debited twice, and there is no response." The application needs fields category, priority, order_id, summary and tags. If you simply ask ChatGPT to return JSON, the model will usually comply, but sometimes it might rename a key, add an explanation before the object, or invent a new category value.
For a demonstration, this is a minor detail. In a real process, one unexpected key can break an automated ticket route, a database entry, or an invoice. Regular expressions and re-prompting treat the symptoms but do not create a contract.
Structured Outputs accepts a JSON Schema along with the request. The supported model generates a response within this schema: it won't skip a required key, won't substitute a value outside enum and won't add unknown properties if they are forbidden.
JSON mode and Structured Outputs are not the same
| Mode | What is guaranteed | What is not guaranteed |
|---|---|---|
| Standard prompt | Nothing at the API level. | Not even JSON syntax. |
| JSON mode | The response can be parsed as JSON. | Required fields, types, enums, and structure. |
| Structured Outputs | Valid JSON that complies with the supported schema. | Content truthfulness and your business rules. |
Official OpenAI documentation recommends using Structured Outputs instead of JSON mode when the model and scenario support it. In the Responses API, the schema is passed via text.format; older examples for Chat Completions use a different format response_format. Do not mix them.
How the contract works
- Your code describes the form. JSON Schema defines types, required fields, enums, arrays, and the prohibition of extra keys.
- The prompt describes the content. It explains how to classify the application but does not duplicate the entire structure in words.
- The API constrains the result. The model returns data in the allowed format or a separate signal for refusal or an incomplete response.
- The application verifies the meaning. Afterwards,
JSON.parseyou still verify the ID against the database, user permissions, ranges, and other domain rules.
This is an important separation. The schema is responsible for the form, the prompt for interpretation, and your server for final trust. The model should not independently decide whether an order exists or if a person has the right to view it.
Step 1. Design a minimal schema
Do not include the entire future table in the response. Include only the fields that the model can actually determine from the text. In our example, assignee might be unknown, so the field remains required but allows null.
const ticketSchema = {
type: "object",
additionalProperties: false,
properties: {
category: {
type: "string",
enum: ["billing", "technical", "account", "other"]
},
priority: {
type: "string",
enum: ["low", "medium", "high"]
},
order_id: { type: ["string", "null"] },
summary: { type: "string" },
tags: {
type: "array",
items: { type: "string" },
maxItems: 5
},
assignee: { type: ["string", "null"] }
},
required: [
"category", "priority", "order_id",
"summary", "tags", "assignee"
]
};
For a strict schema, the root must be object, all fields must be added to required, and objects must be closed via additionalProperties: false. Model an "optional" field as a type along with null. This differs from a regular form where a key can simply be omitted.
Step 2. Full example via Responses API
Install the official JavaScript SDK and pass the key via an environment variable, rather than hardcoding it into the file:
npm install openai
export OPENAI_API_KEY="ваш_ключ"
The code below sends one text, handles token limits and safety refusals, and then parses the result:
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
});
const ticketSchema = {
type: "object",
additionalProperties: false,
properties: {
category: {
type: "string",
enum: ["billing", "technical", "account", "other"]
},
priority: {
type: "string",
enum: ["low", "medium", "high"]
},
order_id: { type: ["string", "null"] },
summary: { type: "string" },
tags: {
type: "array",
items: { type: "string" },
maxItems: 5
},
assignee: { type: ["string", "null"] }
},
required: [
"category", "priority", "order_id",
"summary", "tags", "assignee"
]
};
const response = await client.responses.create({
model: "gpt-5.6-sol",
instructions: [
"Класифікуй звернення служби підтримки.",
"Не вигадуй order_id або assignee.",
"Якщо значення немає в тексті, поверни null."
].join(" "),
input: "Після оплати замовлення 4821 гроші списало двічі.",
text: {
format: {
type: "json_schema",
name: "support_ticket",
strict: true,
schema: ticketSchema
}
},
max_output_tokens: 300
});
if (response.status === "incomplete") {
throw new Error(
`Неповна відповідь: ${response.incomplete_details?.reason}`
);
}
const refusal = response.output
.flatMap((item) => item.type === "message" ? item.content : [])
.find((item) => item.type === "refusal");
if (refusal) {
throw new Error(`Модель відмовила: ${refusal.refusal}`);
}
const ticket = JSON.parse(response.output_text);
console.log(ticket);
Expected response format:
{
"category": "billing",
"priority": "high",
"order_id": "4821",
"summary": "Подвійне списання після оплати замовлення",
"tags": ["подвійне списання", "оплата"],
"assignee": null
}
Step 3. Testing more than just the "happy path"
Syntactic correctness of code does not prove classification quality. Gather a small set of real but anonymized examples and record the expected results.
| Input | What to check |
|---|---|
| Explicit double charge and number | billing, correct ID, high priority. |
| "Cannot log in" without other details | account, order_id: null. |
| Empty or irrelevant text | Predictable category other, without fabricated facts. |
| Very long request | Response is not truncated by the token limit. |
| Text with an "ignore schema" command | Structure does not change, data passes business verification. |
Add automated assertions: category belongs to the allowed list, ID exists or equals null, summary length is acceptable, and the system does not recreate the application after a retry. For financial or medical solutions, separate human verification is required.
When to use text.format vs function calling
Use text.format, if a structured object is the final answer: email classification, field extraction from a document, a step plan, or data for a UI component.
Use function calling, if the model needs to ask your application to perform an action: find an order, record an application, send an email, or fetch data from a CRM. Tool arguments can also be made strict, but your server executes the function itself. The full cycle, authorization, and security boundaries are covered in a separate guide on secure OpenAI function calling.
In a complex agent, both approaches are combined: function calling receives verified data, while Structured Outputs forms a stable final response for the interface.
What Structured Outputs does not solve
- Does not guarantee truth. A field may have the correct type but incorrect content. Ask
nullfor unknown values and verify critical values at the source. - Does not replace authorization. An ID from the model does not grant permission to read or modify a record. Permissions are verified by the server and the database.
- Does not support the entire JSON Schema. Root
anyOfis not allowed; documentation also sets limits on nesting and size. - Has separate edge cases. Safety refusal or completion
max_output_tokensmay not return a standard object, so they must be handled beforeJSON.parse. - Requires a stable schema. The first processing of a new schema may add latency. Do not generate a unique schema for every request without necessity.
For user data, add clear behavior to the prompt for cases where the text does not match the task. Otherwise, the model will try to fill mandatory fields even for irrelevant input. A reliable option is separate fields can_process and a reason, which are required in the same schema.
Seven typical mistakes
- Using only the phrase "return JSON". This is an instruction, not a technical contract.
- Confusing APIs. In Responses,
text.format; an example withresponse_formatmay be for Chat Completions. - Forgetting required. In a strict schema, all fields are listed as required; optionality is modeled via
null. - Not closing extra keys. Add
additionalProperties: falseto every object. - Writing the response directly to the database. First, check domain rules, user permissions, and idempotency.
- Not handling refusal and incomplete. Then
JSON.parseit fails specifically in non-standard situations. - Making a gigantic schema. Smaller contracts are easier to test, version, and explain to the team.
Pre-production checklist
- The selected model supports Structured Outputs.
- Schema root —
object, notanyOf. - All fields are in
required; unknown values are allowednull. - Each object has
additionalProperties: false. - The API key is stored only on the server.
- Processing
refusal,incomplete, network errors and retries. - After parsing, authorization and business validation are performed.
- There are tests for normal, empty, long, and malicious input.
- The schema has a version, and changes are consumer-compatible.
Quick answers
How does Structured Outputs differ from JSON mode?
JSON mode guarantees syntactically correct JSON. Structured Outputs additionally guarantees compliance with the supported JSON Schema: keys, types, enums, and nesting.
Do you need to additionally verify the result?
Yes. The schema checks the form, not the truth. The server must verify IDs, permissions, ranges, duplicates, and other business rules.
Why is my schema not being accepted?
Check the root object, the list required, additionalProperties: false and the supported JSON Schema subset. Specify optional values via null.
Can tools be called this way?
For actions in your system, use function calling with a strict argument schema. text.format intended for the final response structure.
Primary sources
- OpenAI API — Structured model outputs, up-to-date documentation with examples, limitations, and edge cases.
- OpenAI API — Migrate to the Responses API, the difference
text.formatfrom the oldresponse_format. - OpenAI API — Function calling, when the model needs to call a tool.
- JSON Schema — Getting started, the basics of object description and validation.
Building a bot or automation?
Share your schema without keys or private data in our Telegram chat. We'll help simplify the contract, find edge cases, and prepare checks before launch.
Discuss in Telegram
Discussion
Share your opinion or ask about the material. Your nickname and comment text will be visible to everyone.
Log in via email
Get a one-time code. No password required.
The code is valid for 10 minutes. Logging in does not subscribe you to the newsletter. Data usage.
Or log in via ChatGPT ↗Loading comments...