Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

Check a response against a JSON schema

Attach a JSON schema to a gateway's output check, so a reply your code cannot parse does not reach your code.

How-to guideLast updated

On this page

Your classifier asks the model for JSON and parses what comes back. One reply in a few hundred arrives with a missing field, an extra key, or a label nobody defined, and the code that reads it either throws or records a wrong answer quietly.

An output check can stop that reply at the gateway. You attach one JSON Schema document to a guardrail binding; the gateway validates the buffered response against it, and a reply that does not satisfy the document is blocked before your process sees it. Your client sees a 403 carrying x-tt-guardrail: blocked and a body naming the binding, not content it has to guess about.

This page assumes you already publish a guardrail profile for the gateway. Configuring a guard is the binding itself, and the check acts on the buffered responses of a saved-connection request, which which guards act on which request defines. Streaming is the one shape it leaves out, and it has its own section here.

Write the schema document first

For the qualified GPT-4o and GPT-4o Mini text-chat routes, your app may also send response_format: {type: "json_schema", json_schema: {name: "ticket_label", strict: true, schema: ...}}. Keep the schema within 32 KiB and the whole request within 65,536 bytes, and send one explicit output limit from 1 to 4,096 tokens. This asks the provider for the shape; the gateway’s output check independently validates the answer. A refused or incomplete answer is still a failed classification, not a label to guess.

The document is ordinary JSON Schema, read as draft 2020-12. This one is the classifier contract the private test estate publishes: the answer is an object carrying a label from a closed set and a confidence between 0 and 1, and nothing else.

{
  "type": "object",
  "additionalProperties": false,
  "required": ["label", "confidence"],
  "properties": {
    "label": {"type": "string", "enum": ["billing", "technical", "account", "other"]},
    "confidence": {"type": "number", "minimum": 0, "maximum": 1}
  }
}

additionalProperties: false is what makes the check worth arming. Without it, a response carrying an extra key satisfies the schema, and “the answer has the shape my code parses” stops being what is enforced.

Three keywords are absent from that document and have to stay absent: $schema, an absolute $id, and any $ref pointing outside the document. Draft 2020-12 is the dialect the gateway reads anyway, and a configuration document may carry no URL at all, so an absolute one is turned away on save. Inline the definitions you would have referenced under $defs.

Attach it to the binding

The schema is part of the guardrail profile, not a resource of its own, so there is no second thing to publish and nothing new to pin. Add a schema object to the binding that names the format.jsonschema detector:

{
  "bindings": [
    {
      "id": "ticket-label-out",
      "category": "format",
      "phases": ["output"],
      "detectors": ["format.jsonschema"],
      "action": "block",
      "mode": "enforce",
      "schema": {
        "document": "<the document above, as a JSON string>",
        "fields": ["choices[0].message.content"]
      }
    }
  ]
}

document carries the schema as a JSON string, so whatever publishes the profile has to escape it. That binding is the one the private test estate arms for its classifier call site; its fields are compiled and armed by the repository’s own tests, and the document string is the block preceding it.

Publish it the way you publish any profile revision: POST …/configuration/v1/guardrail-profiles for a new profile, PUT …/configuration/v1/guardrail-profiles/{id} with the expected_version you last read for a change to an existing one. A gateway picks the profile up when its next release is deployed.

Set fields. An empty restriction validates every text field the engine presents, which for a chat completion includes a system fingerprint and any tool-call argument blob. A correct answer then produces violations on fields nobody meant to constrain.

The limits

Each of these is checked when you save the profile, so you meet them at the keyboard rather than at a deploy.

Limit Value
schema document at most 32 KiB
field restrictions at most 16 paths, each at most 200 characters
schema checks per composed profile one
checks on one response value under a repeating keyword at most 32
checks over the whole response, applied once at most 8,192
anyOf/oneOf/allOf/not/if nesting depth at most 8
$schema, absolute $id, remote $ref omit them; a URL anywhere in a configuration document is turned away as forbidden_content
dialect JSON Schema 2020-12, which is the default and cannot be selected
mandatory and platform bindings carry no workspace schema

One schema per composed profile is a consequence of how detectors are armed: a compiled profile arms one instance of a detector id, so a second schema binding would leave one of the two validating against the other’s document while reporting itself armed. Split the two gateways, or combine the two documents.

Two more limits bound how expensive the schema itself may be to check, and both are enforced at save, not at request time. Inside a repeating position — items, additionalProperties, patternProperties, contains, propertyNames, or an unevaluated* keyword — one response value may cost at most 32 checks, because that cost is paid once per element the model sends. Over the whole response, checked once, the total is capped at 8,192 checks — this is the one that catches a schema with no repeating keyword at all, such as a $ref chain that nests into itself. anyOf, oneOf, allOf, not and if/then/else may nest at most 8 deep; a $ref hop into $defs does not add to that depth, but a large enum list does add to both check totals, one check per listed value. A schema whose worst-case cost is over either check limit is turned away with the sentence “This schema is too complex to check on every response:”, followed by which limit it hit, the number it reached, and what to change. A $ref cycle or a $dynamicRef/$recursiveRef is refused outright with the same opening sentence, because its cost depends on how deeply the response is nested rather than on the schema itself; inline the recursive part to the depth you need.

Under heavy concurrent load, the check itself can answer unavailable rather than wait behind other schema validations already running, and the binding’s own configured failure behavior then decides the request.

What a bad schema says

Every one of these is an invalid_value problem on the schema subtree of the binding, raised when the profile revision is saved. The path is what an editor keys on; the sentence is what it shows.

What you did Path What it says
named the detector and attached no schema /bindings/{i}/schema This response check needs a JSON schema. Add the schema the response must satisfy.
attached a schema to a binding that names no check taking one /bindings/{i}/schema a schema configures the format.jsonschema detector only; this binding does not name it
went over the size limit /bindings/{i}/schema/document This schema is over the 32 KiB limit for a response check. Reduce it, or validate fewer fields.
wrote something that is not a JSON object /bindings/{i}/schema/document schema is not valid JSON · a JSON Schema document is a JSON object
left a reference to a document outside this one /bindings/{i}/schema/document the format.jsonschema check cannot be armed from this schema, followed by the loader’s own reason and the reference it turned away
worst-case cost is over the repeated-check, total-check or nesting-depth limit /bindings/{i}/schema/document This schema is too complex to check on every response: followed by which limit it hit, the number it reached, and what to change
the schema is recursive (a $ref cycle, or $dynamicRef/$recursiveRef) /bindings/{i}/schema/document This schema is too complex to check on every response: it uses $dynamicRef/$recursiveRef, or a $ref in it points back into itself, so its cost depends on the response rather than the schema. Inline the recursive part to the depth you need
armed two schema checks in one composed profile /bindings/{i}/detectors/{j} A gateway can carry one response-schema check. Remove the other, or combine the schemas.

A profile that reaches a deploy carrying one of these is not deployed either: the deployment fails validation_failed with a 422 and the same diagnostics. Nothing is armed half way, and a malformed schema takes its whole profile with it, which the gateway’s protection read reports as profile_not_compilable.

Streaming is not schema-checked

A schema is a claim about a whole document, and no incremental matcher can decide a half-arrived one. So format.jsonschema declares the output phase alone, and a binding that names stream beside it does not compile. “Block a stream on a schema” is not a configuration you can write.

What follows for a streamed request to a gateway whose schema check is armed:

  1. The response is delivered frame by frame and the schema is not consulted.
  2. The gateway’s protection read carries the reason stream_unsupported on that row, so the gap is published rather than left for you to infer.
  3. Detectors that do carry an incremental matcher still act. Email redaction is the one the test estate arms: pii.email holds emitted events back inside its window and rewrites an address that spans two frames.
  4. Bytes already emitted cannot be recalled. A hold-back window is where a stream can still be changed, and everything the client has already received is the client’s.

Take the schema check as a reason to keep a structured call site non-streaming. A call whose whole value is a parseable object gains little from incremental delivery, and it gains a check it cannot otherwise have. Phases & streaming is the window, the caps, and what happens when the matcher runs out of room.

What can go wrong

The schema and the request’s response_format drift apart. The check is server-side and knows nothing about what your client asked the provider for. If you tighten the gateway’s schema and not the request, or the other way round, every reply is blocked and the model was doing what it was told. Change the two together.

A correct answer is blocked because fields was empty. The schema was written for the assistant’s message and applied to every string the engine presents. Name the field.

A rollout arms the check before the model reliably satisfies it. The stage that answers this is monitor: the binding runs, discloses, and changes no byte, and the evidence surface tells you what the gateway would have blocked. Proving it before you arm it is that sequence.

What this page doesn’t cover

Availability. The schema check is part of this private test release. It is armed through the profile a gateway’s release pins, and the wording above comes from the validator that turns a bad document away.