# Structured output

> Constrain a chat response to a zod schema and get a typed object back.

Source: https://maincode.com/docs/client-sdk-structured-output
Section: Client SDK · Matilda documentation

---

Structured output constrains the model's response to a JSON Schema, server-side (grammar-constrained decoding), and then validates it client-side against your zod schema. Pass a zod schema, receive a fully-typed object — no prompt engineering, no brittle JSON extraction.

## `chat.streamObject(params, schema, options?)`

Streams exactly like `chat.stream()` — you receive every `MatildaChatStreamEvent` — plus one final event with the parsed, schema-validated object. The `schema` argument is any zod schema (`z` is bundled with the SDK); the SDK converts it to JSON Schema and constrains generation server-side.

```ts title="structured.ts"
import { z } from 'zod';

const recipe = z.object({
  name: z.string(),
  prepTimeMinutes: z.number(),
  ingredients: z.array(z.string()),
});

for await (const event of client.chat.streamObject(
  { input: 'Give me a recipe for pavlova.' },
  recipe,
)) {
  if (event.type === 'response.output_text.delta') {
    process.stdout.write(event.delta); // raw JSON streaming in
  }
  if (event.type === 'object') {
    console.log('\nValidated:', event.object); // typed as z.infer<typeof recipe>
  }
}
```

The final event:

```ts
{ type: 'object'; object: T } // T = z.infer<typeof schema>
```

## `chat.createObject(params, schema, options?)`

Non-streaming convenience. Returns a `MatildaObjectResponse<T>` — everything `chat.create()` returns, plus the validated `object`.

```ts title="invoice.ts"
const response = await client.chat.createObject(
  { input: 'Extract the invoice total: $1,250.00 AUD due 30 Sep.', conversationId },
  z.object({ total: z.number(), currency: z.string() }),
);

console.log(response.object.total);    // 1250 (number)
console.log(response.object.currency); // "AUD" (string)
console.log(response.outputText);      // raw JSON text as returned
```

### `MatildaObjectResponse<T>`

Extends `MatildaChatResponse` with one additional field:

| Field | Type | Description |
| - | - | - |
| `object` | `T` | The response text parsed as JSON and validated against your schema. |

## Raw JSON Schema via `responseSchema`

If you don't want zod validation, pass a stringified JSON Schema directly as `responseSchema` on any chat call:

```ts title="raw-schema.ts"
const response = await client.chat.create({
  input: 'List three Australian birds.',
  responseSchema: JSON.stringify({
    type: 'object',
    properties: { birds: { type: 'array', items: { type: 'string' } } },
    required: ['birds'],
    additionalProperties: false,
  }),
});
JSON.parse(response.outputText); // guaranteed valid, schema-conforming JSON
```

With `responseSchema` set, the response text is guaranteed to be valid JSON conforming to the schema — but parsing and validation are up to you.

## OpenAI-compatible endpoint

The OpenAI-compatible endpoint (`POST /api/v1/chat/completions`) also honours structured output via the standard `response_format` parameter, so the OpenAI JS SDK's structured-output option works against Matilda as-is:

- `{ "type": "json_schema", "json_schema": { "name": "...", "schema": {...} } }` — grammar-constrained to your schema (the schema is applied with `strict: true` server-side; the `strict` and `name` fields you supply are re-wrapped downstream).
- `{ "type": "json_object" }` — guarantees valid JSON output without a schema (OpenAI JSON mode).

> **Note** — **Safety replace and structured output.** If the server replaces the output mid-stream (safety filter), `streamObject` throws `SafetyReplaceError` — the replacement text is in `.message` and the triggering categories in `.categories`. Deltas already yielded to your consumer are not rolled back; if you render streamed JSON, handle `response.output_text.replace` events (or choose non-streaming `createObject`) to avoid showing half-rendered output that is later discarded.

> **Caution** — **Truncation throws.** If the stream is truncated before the JSON completes, both helpers throw `MatildaObjectParseError` with the partial text in `.raw`. See [Error handling](https://maincode.com/docs/client-sdk-error-handling).

> **Caution** — **Stream errors throw.** If the server emits an error event mid-stream, both helpers throw an `Error` with the server's error code and message (`${code}: ${message}`).
