BuildJSON output

JSON output

⚠️
Preview — this feature is being built; the design below is the spec.

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:

typeGuarantee
json_objectThe reply is one valid JSON object. Its fields are up to the model and your instructions.
json_schemaThe 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

FieldTypeRequiredDetails
response_format.typestringYestext, json_object or json_schema.
response_format.json_schema.namestringWith json_schema1 to 64 characters: letters, digits, _, -.
response_format.json_schema.descriptionstringNoWhat the object represents. The model reads it.
response_format.json_schema.schemaobjectWith json_schemaA JSON Schema (draft 2020-12 subset below).
response_format.json_schema.strictbooleanNoDefaults 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

SupportedNot supported
type (object, array, string, number, integer, boolean, null)$ref to external URLs
properties, required, additionalProperties: falsepatternProperties
enum, constif / then / else
items, minItems, maxItemsdependentSchemas
anyOfallOf, 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

Unified envelope.

StatuscodeparamCause
400invalid_response_formatresponse_format.typeUnknown type.
400json_not_mentionedmessagesjson_object without the word JSON in any message.
400invalid_schemaresponse_format.json_schema.schemaNot valid JSON Schema, or uses an unsupported feature. message names it.
400schema_not_strictpath inside the schemastrict: true but an object is missing additionalProperties: false or a required entry.
400schema_too_largeresponse_format.json_schema.schemaOver the limits below.
422output_schema_violationpath inside the outputThe 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

LimitValue
Schema size16 KB serialised
Properties in total200
Nesting depth8
enum values in total500

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 header x-syntic-schema-retries: 1.
Was this page helpful?