Building

Structured outputs

Ask for JSON and get JSON — not JSON wrapped in a code fence, not JSON with a sentence in front of it. All three models support this.

With a schema#

The useful form. You give a JSON Schema and the response conforms to it, so your parse can be a plain json.loads rather than a defensive mess.

schema = {
"type": "object",
"properties": {
"invoice_number": {"type": "string"},
"total": {"type": "number"},
"currency": {"type": "string", "enum": ["INR", "USD", "EUR"]},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"amount": {"type": "number"},
},
"required": ["description", "amount"],
},
},
},
"required": ["invoice_number", "total", "currency", "line_items"],
"additionalProperties": False,
}
 
response = client.chat.completions.create(
model="upfyn-yuva",
messages=[{"role": "user", "content": invoice_text}],
response_format={
"type": "json_schema",
"json_schema": {"name": "invoice", "schema": schema, "strict": True},
},
)
 
import json
invoice = json.loads(response.choices[0].message.content)

Writing a schema that works

  • Mark everything you actually need as required. An optional field is a field you will find missing at 2am.
  • Set additionalProperties: false so you get your shape and nothing else.
  • Use enum wherever the value comes from a fixed set — it removes a whole class of near-miss strings like "Rupees" instead of "INR".
  • Describe fields whose meaning is not obvious from the name. The schema is a prompt.
  • Keep nesting shallow. Deep schemas cost tokens and give the model more ways to go wrong.

Just valid JSON#

If you do not care about the shape, ask for json_object. You get parseable JSON with no guarantee about its keys.

Loose mode
response_format={"type": "json_object"} # valid JSON, any shape

Say what you want in the prompt too

In loose mode the prompt is the only thing describing the shape. Name the keys you expect and show one example.

The response is still a string#

message.content holds JSON text. You parse it. Wrap that parse in a try — a truncated response is still a string, and it will not parse.

Truncation looks like a parse error

If finish_reason is "length", the model ran out of output tokens mid-object. The fix is a bigger max_tokens, not a more forgiving parser. Check finish_reason before you blame the JSON.

Structured outputs or tool calling?#

  • You want data back — use response_format. One call, one object.
  • You want the model to do something — use tools. It is a loop, and your code runs in the middle of it.

Extraction, classification and summarising into fields are all the first kind. Reaching for tool calling to get structured data works, but it costs you an extra round trip for nothing.