> ## Documentation Index
> Fetch the complete documentation index at: https://docs.impellabs.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Structured outputs

> Asking for JSON that validates against your schema — and what happens when the model does not produce it.

Set `response_format` and the model's reply is parsed, pruned to your schema's
own keys, validated against it, repaired at most once, and returned as a `json`
output item — or refused with a **422** that names the paths that did not match.

```json theme={null}
{
  "agent": "agt_triage",
  "input": "Charged twice on invoice 8841, enterprise customer, very unhappy.",
  "response_format": {
    "type": "json_schema",
    "name": "triage",
    "schema": {
      "type": "object",
      "properties": {
        "urgency": { "type": "string", "enum": ["low", "normal", "high"] },
        "team":    { "type": "string" },
        "invoice": { "type": "string" }
      },
      "required": ["urgency", "team"]
    }
  }
}
```

```json theme={null}
{
  "id": "run_…",
  "status": "completed",
  "output": [
    { "type": "json", "json": { "urgency": "high", "team": "billing", "invoice": "8841" } }
  ],
  "usage": { "…": "…" }
}
```

The output item type is `json` and the value is under the `json` key. `output` is
still an array — a run can produce prose *and* a structured object.

## Three formats

| `response_format`                          | Behaviour                              |
| ------------------------------------------ | -------------------------------------- |
| `"text"`                                   | The default. Prose.                    |
| `"json_object"`                            | A JSON object, no schema asserted.     |
| `{ "type": "json_schema", "schema": {…} }` | Validated against your schema, or 422. |

You may send the short string form (`"response_format": "json_schema"`) or the
object form. The object form takes `name` (up to 64 characters), `schema`
(required for `json_schema`) and `strict`.

<Note>
  `strict` is accepted **only as `true`**. There is no mode in which a schema is
  requested and not enforced. A caller told their schema was honoured when it was
  not is the single outcome this whole feature exists to prevent, and a flag that
  turns validation off is that outcome with a name.
</Note>

## The schema subset

The validator is a closed keyword set, not a full JSON Schema engine. Accepting a
keyword and then ignoring it is how an API quietly returns something other than
what was asked for.

**Accepted:**

```
type · properties · required · items · enum · additionalProperties
minimum · maximum · minLength · maxLength · minItems · maxItems
format · title · description
```

Types: `object`, `array`, `string`, `number`, `integer`, `boolean`, `null`.
Formats: `email`, `date`, `date-time`, `uri`, `uuid`.

**Refused, each with a 400 naming the keyword and the reason:**

| Keyword                          | Why                                                                                     |
| -------------------------------- | --------------------------------------------------------------------------------------- |
| `$ref`, `$defs`, `definitions`   | References are not supported: a schema must be self-contained. A `$ref` is a fetch.     |
| `pattern`, `patternProperties`   | A caller-supplied regex is an unbounded amount of work for this server to do.           |
| `oneOf`, `anyOf`, `allOf`, `not` | Combinators are not supported: describe one shape. No prompt expresses them faithfully. |
| `if`, `then`, `else`             | Conditional schemas are not supported.                                                  |
| `const`                          | A constant is not something to extract.                                                 |
| `default`                        | A default would put a value in the result that is not in the source.                    |

Bounds: 16 KB of schema, 6 levels deep, 200 nodes, 100 properties per object, 200
enum values. Property names match `[A-Za-z_][A-Za-z0-9_-]{0,63}` — the characters
that *are* the evidence-path grammar (`.` and `[]`) cannot also appear in a name,
or an entry for `a.b` would be indistinguishable from one for `b` inside `a`.

### Two deliberate departures from JSON Schema

<AccordionGroup>
  <Accordion title="`format` is asserted, not annotated">
    In JSON Schema, `format` is a hint a validator may ignore. Here it is
    checked. A caller who writes `"format": "email"` is telling us what they will
    do with the value, and handing them `"n/a"` because the spec permits it would
    be the wrong kind of correct.
  </Accordion>

  <Accordion title="A `required` field that came back `null` is missing">
    JSON Schema counts the key as present. `{"full_name": null}` is not an
    answer, it is the absence of one wearing a key.

    If you genuinely want "present, possibly null", write
    `"type": ["string", "null"]`. The subset supports it and it then passes.
  </Accordion>
</AccordionGroup>

### `additionalProperties` prunes rather than fails

It defaults to **false**, and the effect is to remove keys you did not ask for
before validation runs. A model volunteering an extra field is the commonest form
of plausible-looking noise, and neither silently returning it nor failing the
whole run over it is the right answer.

## Cost is knowable before the run starts

One repair pass by default; two at the absolute most. So one structured run costs
at most `1 + max_repairs` model calls, and a bad schema cannot turn into an
open-ended spend. There are no unlimited repair loops.

A model that produced the wrong shape twice, given the exact failing paths, is
not going to produce the right one on the third try — and each attempt is a full
turn, with knowledge, memory and tools.

## When it fails

<Steps>
  <Step title="A schema this runtime cannot enforce is a 400, before any spend">
    The schema is checked **when the request is parsed** — before authorization,
    before the idempotency claim, before a single model call. So an unsupported
    keyword costs you nothing.

    ```json theme={null}
    { "error": { "code": "invalid_request", "message": "…\"pattern\" …", "request_id": "req_…" } }
    ```
  </Step>

  <Step title="Output that will not validate is a 422">
    ```json theme={null}
    {
      "error": {
        "code": "structured_output_failed",
        "message": "urgency: not one of the permitted values; team: required field missing",
        "request_id": "req_…"
      },
      "id": "run_…",
      "status": "failed",
      "trace_id": "trc_…"
    }
    ```

    At most five failing paths are named — enough to fix a prompt, few enough
    that a wildly wrong answer does not produce a paragraph.
  </Step>
</Steps>

<Warning>
  **A 422 names the paths and never the values.** You do not get the invalid
  object back, not even "just to see".

  Handing it over would be unvalidated output delivered through the error
  channel, and an integration that reads it there has silently accepted exactly
  what the validation exists to refuse.
</Warning>

Rejected output does not reach the transcript either. In a
[conversation](/v2/conversations), only the validated object is ever written.

## Streaming a structured run

Fragments arrive as `response.output_json.delta` rather than
`response.output_text.delta`. They are still advisory — read the validated object
off `response.completed`. A partial JSON string is not JSON.

## Reports

`POST /reports` accepts `"format": "json"` with a `schema` and honours it the same
way. See [Reports](/v2/reports).
