JSON output
When a program, not a person, reads the reply, you want JSON you can parse every time. response_format asks the API to guarantee it, in two levels:
type | Guarantee |
|---|---|
json_object | The reply is one valid JSON object. Its fields are up to the model and your instructions. |
json_schema | The reply is valid JSON and matches the JSON Schema you supply. |
text (default) | No guarantee. |
json_object ships first, json_schema follows. Today the field is ignored.
When to use it
- Extracting fields from emails, invoices, forms.
- Classifying text into fixed labels.
- Feeding one model step’s output into code or another step.
If the model must also decide whether to produce structured data, a tool with strict arguments is often the better fit.
Endpoint and auth
POST https://app.syntic.ai/v1/chat/completions (also on Messages) with Authorization: Bearer sk_live_....
Request fields
| Field | Type | Required | Details |
|---|---|---|---|
response_format.type | string | Yes | text, json_object or json_schema. |
response_format.json_schema.name | string | With json_schema | 1 to 64 characters: letters, digits, _, -. |
response_format.json_schema.description | string | No | What the object represents. The model reads it. |
response_format.json_schema.schema | object | With json_schema | A JSON Schema (draft 2020-12 subset below). |
response_format.json_schema.strict | boolean | No | Defaults to true. When false, the schema guides the model but only JSON validity is guaranteed. |
With json_object, mention JSON somewhere in your instructions. Requests that do not return 400 json_not_mentioned, because a model told nothing about the expected shape tends to produce poor objects.
Supported schema features
| Supported | Not supported |
|---|---|
type (object, array, string, number, integer, boolean, null) | $ref to external URLs |
properties, required, additionalProperties: false | patternProperties |
enum, const | if / then / else |
items, minItems, maxItems | dependentSchemas |
anyOf | allOf, not |
minimum, maximum, minLength, maxLength, pattern, format (date-time, date, email, uuid) | Recursive schemas deeper than 5 levels |
Local $defs and $ref: "#/$defs/..." |
With strict: true, every object must set additionalProperties: false and list every property in required (use "type": ["string","null"] for optional values). The API checks this before running the model.
How it works
Constrained generation where available
If the model supports constrained decoding, output can only follow the schema.
Validation otherwise
Otherwise Syntic validates the finished reply against the schema.
One automatic retry
On a mismatch, Syntic retries once, telling the model what was wrong. You are billed for both attempts.
Error if still wrong
If the retry also fails, the request returns 422 output_schema_violation with the validator’s first error.
Response
The JSON is in the usual place, as a string: choices[0].message.content. Parse it.
{
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "{\"invoice_number\":\"4471\",\"total\":87.0,\"currency\":\"GBP\",\"due_date\":\"2026-11-01\"}"
},
"finish_reason": "stop"
}],
"usage": { "prompt_tokens": 212, "completion_tokens": 31, "total_tokens": 243 }
}If the reply hits max_tokens the JSON is cut off; finish_reason is length and the content will not parse. A refusal reply contains plain text, not JSON. Always check finish_reason first.
In a stream, the JSON arrives in text fragments like any reply. Parse after the last chunk.
Examples
curl https://app.syntic.ai/v1/chat/completions \
-H "Authorization: Bearer $SYNTIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "Amara S1.2",
"stream": false,
"messages": [
{ "role": "system", "content": "Extract invoice fields." },
{ "role": "user", "content": "Invoice 4471 from Hartley Nurseries. Total £87.00, due 1 Nov 2026." }
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "invoice",
"strict": true,
"schema": {
"type": "object",
"properties": {
"invoice_number": { "type": "string" },
"total": { "type": "number" },
"currency": { "type": "string", "enum": ["GBP", "EUR", "USD"] },
"due_date": { "type": ["string", "null"], "format": "date" }
},
"required": ["invoice_number", "total", "currency", "due_date"],
"additionalProperties": false
}
}
}
}'Errors
| Status | code | param | Cause |
|---|---|---|---|
| 400 | invalid_response_format | response_format.type | Unknown type. |
| 400 | json_not_mentioned | messages | json_object without the word JSON in any message. |
| 400 | invalid_schema | response_format.json_schema.schema | Not valid JSON Schema, or uses an unsupported feature. message names it. |
| 400 | schema_not_strict | path inside the schema | strict: true but an object is missing additionalProperties: false or a required entry. |
| 400 | schema_too_large | response_format.json_schema.schema | Over the limits below. |
| 422 | output_schema_violation | path inside the output | The model’s reply still did not match after one retry. |
{
"error": {
"type": "invalid_request",
"code": "output_schema_violation",
"message": "Output did not match schema: /total must be number.",
"param": "/total"
},
"request_id": "req_01JBXA2"
}Limits
| Limit | Value |
|---|---|
| Schema size | 16 KB serialised |
| Properties in total | 200 |
| Nesting depth | 8 |
enum values in total | 500 |
Build notes for the platform team
- One validator module, shared with strict tools. Compile and cache schemas by hash.
- The retry is server-side and invisible except in
usage(both attempts counted) and headerx-syntic-schema-retries: 1.