# Structured output

> Constrain a chat response to a pydantic model and get a validated object back.

Source: https://maincode.com/docs/python-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 pydantic model. Pass a pydantic model class (or a plain JSON-schema `dict`), receive a validated object — no prompt engineering, no brittle JSON extraction. The SDK duck-types on `model_json_schema`, so any pydantic v2 version works and pydantic stays an optional dependency.

## `chat.stream_object(schema, *, input=...)`

Streams exactly like `chat.stream()` — you receive every `ChatEvent` — plus one final `ObjectEvent` with the parsed, schema-validated object.

```python title="structured.py"
from pydantic import BaseModel


class Recipe(BaseModel):
    name: str
    prep_time_minutes: float
    ingredients: list[str]


async def main():
    async with MatildaClient(token="...") as client:
        async for event in client.chat.stream_object(
            Recipe,
            input="Give me a recipe for pavlova.",
        ):
            if event.type == "response.output_text.delta":
                print(event.delta, end="", flush=True)  # raw JSON streaming in
            if event.type == "object":
                recipe = event.object  # a validated Recipe instance
                print(f"\nValidated: {recipe.name}")
```

The final event:

```python
@dataclass(frozen=True)
class ObjectEvent[T]:
    object: T  # your pydantic model (or parsed JSON for dict schemas)
    # type = "object"
```

## `chat.create_object(schema, *, input=...)`

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

```python title="invoice.py"
class Invoice(BaseModel):
    total: float
    currency: str


async def main():
    async with MatildaClient(token="...") as client:
        response = await client.chat.create_object(
            Invoice,
            input="Extract the invoice total: $1,250.00 AUD due 30 Sep.",
            conversation_id=conversation_id,
        )

        print(response.object.total)      # 1250.0 (float)
        print(response.object.currency)   # "AUD" (str)
        print(response.output_text)       # raw JSON text as returned
```

### `MatildaObjectResponse`

Extends `MatildaChatResponse` with one additional field:

| Field | Type | Description |
| - | - | - |
| `object` | `T \| None` | The response text parsed as JSON and validated against your schema (a pydantic model instance for model classes; the parsed JSON for plain dict schemas). |

## Raw JSON Schema via `response_schema`

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

```python title="raw_schema.py"
import json


async def main():
    async with MatildaClient(token="...") as client:
        response = await client.chat.create(
            input="List three Australian birds.",
            response_schema=json.dumps({
                "type": "object",
                "properties": {"birds": {"type": "array", "items": {"type": "string"}}},
                "required": ["birds"],
                "additionalProperties": False,
            }),
        )
        data = json.loads(response.output_text)  # guaranteed valid, schema-conforming JSON
```

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

Local `$ref`/`$defs` (pydantic's nested-model output) are inlined client-side before the schema is sent — the server's grammar compiler does not resolve `#/$defs/...` pointers. A genuinely recursive shape degrades to unconstrained at the recursion point, which client-side pydantic validation still catches.

## 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 Python 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), `stream_object` and `create_object` raise `SafetyReplaceError` — the replacement text is in `str(err)` 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 `create_object`) to avoid showing half-rendered output that is later discarded.

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

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