OpenAI API · practical guide

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.

September 29, 202614 min readComplexity: medium
Author's illustration for the topic: How to get JSON from OpenAI API: Structured Outputs
Illustration created with AI · Vibe Coding
What you will get. Let's build a small JavaScript script that converts client request text into predictable JSON for a CRM. You need Node.js 20+, an OpenAI API key, the package 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

ModeWhat is guaranteedWhat is not guaranteed
Standard promptNothing at the API level.Not even JSON syntax.
JSON modeThe response can be parsed as JSON.Required fields, types, enums, and structure.
Structured OutputsValid 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

  1. Your code describes the form. JSON Schema defines types, required fields, enums, arrays, and the prohibition of extra keys.
  2. The prompt describes the content. It explains how to classify the application but does not duplicate the entire structure in words.
  3. The API constrains the result. The model returns data in the allowed format or a separate signal for refusal or an incomplete response.
  4. The application verifies the meaning. Afterwards, JSON.parse you 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
}
Do not copy the API key to the frontend. Make the OpenAI call on the server or in a secure serverless function. If the key has already ended up in a browser, GitHub, or a screenshot, revoke it and create a new one. A detailed plan is in our API key security guide.

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.

InputWhat to check
Explicit double charge and numberbilling, correct ID, high priority.
"Cannot log in" without other detailsaccount, order_id: null.
Empty or irrelevant textPredictable category other, without fabricated facts.
Very long requestResponse is not truncated by the token limit.
Text with an "ignore schema" commandStructure 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 null for 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 anyOf is not allowed; documentation also sets limits on nesting and size.
  • Has separate edge cases. Safety refusal or completion max_output_tokens may not return a standard object, so they must be handled before JSON.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

  1. Using only the phrase "return JSON". This is an instruction, not a technical contract.
  2. Confusing APIs. In Responses, text.format; an example with response_format may be for Chat Completions.
  3. Forgetting required. In a strict schema, all fields are listed as required; optionality is modeled via null.
  4. Not closing extra keys. Add additionalProperties: false to every object.
  5. Writing the response directly to the database. First, check domain rules, user permissions, and idempotency.
  6. Not handling refusal and incomplete. Then JSON.parse it fails specifically in non-standard situations.
  7. 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, not anyOf.
  • All fields are in required; unknown values are allowed null.
  • 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

  1. OpenAI API — Structured model outputs, up-to-date documentation with examples, limitations, and edge cases.
  2. OpenAI API — Migrate to the Responses API, the difference text.format from the old response_format.
  3. OpenAI API — Function calling, when the model needs to call a tool.
  4. 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

Was this helpful? Pass it on

Send it to someone with a similar task.

Share in Telegram ↗

Read via RSS · Weekly newsletter in Ukrainian

Discussion

Share your opinion or ask about the material. Your nickname and comment text will be visible to everyone.

Loading comments...

Discuss in Telegram