Skip to content
RU

Structured output

When you need an object from the model rather than prose — a product card, a classification result, arguments for a function — POST /v1/chat/completions takes a response_format parameter. It is passed to the upstream as is, so it behaves exactly like the OpenAI API. Billing is the usual per-token kind — there is no surcharge for structured output.

response_format combines freely with the other parameters: stream, reasoning_effort and tools can all appear in the same request — call a tool and get the function’s answer back as schema-conformant JSON, for example.

The {"type": "json_object"} mode obliges the model to answer with valid JSON: no preamble, no markdown wrapper, no mid-object truncation.

One requirement worth following: the word “JSON” must appear in the prompt itself. That is OpenAI’s rule — some models, not seeing it, refuse to answer or the request fails with a 400. The simplest fix is to say it directly: “Return the answer as JSON with the fields …”.

import json
from openai import OpenAI
client = OpenAI(
base_url="https://api.mixen.ai/v1",
api_key="mxn-...",
)
resp = client.chat.completions.create(
model="gpt-5.6-luna",
messages=[{
"role": "user",
"content": (
"Extract the sender, the recipient and the transfer amount from the email. "
"Return the answer as JSON with the fields from, to, amount (a number, in rubles).\n\n"
"Email: «Ivan, hi! Please transfer 1500 rubles to Katya by Friday. — Maria»"
),
}],
response_format={"type": "json_object"},
)
data = json.loads(resp.choices[0].message.content)
print(data["from"], data["to"], data["amount"])

message.content arrives as a string roughly like this:

{"from": "Maria", "to": "Katya", "amount": 1500}

Note that content stays a string — Mixen and the upstream guarantee valid JSON inside it, but the client does the parsing (json.loads). That is the general rule of structured output: the guarantee lives on the model’s side, the parsing lives in your code.

Only syntax is guaranteed. Which fields end up inside and of what types is decided by the prompt; for a hard schema, see the next mode.

json_schema — the response follows a schema

Section titled “json_schema — the response follows a schema”

The json_schema mode is stricter: the model must build the answer according to a schema — exactly those fields, exactly those types. The schema travels inside response_format, and "strict": true turns on strict generation.

A pydantic model is a convenient way to describe the schema: .model_json_schema() yields a ready dict, and the same model then validates the response:

import json
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI(
base_url="https://api.mixen.ai/v1",
api_key="mxn-...",
)
class ProductCard(BaseModel):
name: str # product name
price: int # price in rubles
tags: list[str] # up to five tags
resp = client.chat.completions.create(
model="gpt-5.6-luna",
messages=[{
"role": "user",
"content": (
"Build a product card: name, price in rubles and up to five tags.\n\n"
"Product: wireless headphones with active noise cancellation, 7,990 ₽"
),
}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "product_card",
"schema": ProductCard.model_json_schema(),
"strict": True,
},
},
)
parsed = json.loads(resp.choices[0].message.content)
card = ProductCard.model_validate(parsed)
print(card.name, card.price, card.tags)

A few notes on the examples:

  • The schema can be written by hand, as in the cURL example — pydantic is optional. For strict mode OpenAI usually requires every field to be listed in required and additionalProperties to be false.
  • model_validate here is a safety net, not a replacement for strict generation: the answer almost always matches the schema already, but checking on your side is cheaper than catching a surprise in production.
  • As in the first mode, message.content is a string. The parse(..., response_model=...) method of the OpenAI SDK assumes the API does the validation — here the upstream does it at generation time, so parsing and checking are your code’s job: an explicit json.loads plus validation.

json_schema with strict is a capability of the specific model, not part of the protocol. A model that lacks it returns 400 with the upstream’s error (error codes and formats are covered in Errors). Two workable moves:

  • Fallback without strict. Describe the schema in words inside the prompt — “Return JSON with the fields name (string), price (integer), tags (array of strings, up to five)” — and keep {"type": "json_object"}. Syntax validity is guaranteed, field conformance is checked by the same pydantic model via model_validate; on a mismatch, simply retry the request.
  • A probe request. Whether a particular model supports strict schemas is fastest to check with one cheap request carrying a minimal schema — then pin the result in your application’s configuration.
  • /v1/chat/completions only. The /v1/messages and /v1/responses endpoints do not accept response_format.
  • message.content is a string. The shape guarantee lives in the upstream; parsing the JSON is always the client’s job.
  • strict depends on the model: some support it, some don’t. There is no universal flag in the catalog — check with a probe request.

A word about the Anthropic protocol: /v1/messages has no structured-output parameter, and that is not a Mixen restriction but how the protocol itself is built. The same effect is assembled there through tool-use: you describe a tool whose input_schema matches the schema you need, force the call via tool_choice, and read the JSON from the call’s arguments.

See also: Chat and streaming and Tool calling.