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 jsoninvoice = 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: falseso you get your shape and nothing else. - Use
enumwherever 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.
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
Iffinish_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.
