---
title: "Structured output"
description: "Force agents to produce validated JSON conforming to a user-provided schema"
icon: "square-code"
---

Structured output forces the agent to produce validated JSON conforming to a schema you provide. The extraction runs as a separate turn after agentic work completes, with automatic retry on validation failure. Schemas are normalized across providers -- the same schema works with Anthropic, OpenAI, and Gemini, with provider-specific lowering handled transparently.

<Note>
This page is the task-first guide. For the low-level schema/type inventory, see [Structured output reference](/reference/structured-output-reference).
</Note>

## What this guide is for

Use this guide when you want to:

- extract validated JSON from an agent run
- choose schema compatibility behavior
- understand retry and warning behavior

## How it works

When `output_schema` is configured on an agent, the execution flow is:

<Steps>
  <Step title="Agentic loop runs normally">
    The agent processes the prompt, calls tools, and iterates until no more tool calls remain.
  </Step>
  <Step title="Extraction turn fires">
    An additional LLM call is made with no tools, temperature `0.0` for deterministic output, and a prompt asking for valid JSON matching the schema.
  </Step>
  <Step title="Validation">
    The response is parsed as JSON and, when the `jsonschema` feature is enabled, validated against the schema using the `jsonschema` crate's `Validator`.
  </Step>
  <Step title="On success">
    `RunResult.structured_output` contains the parsed `serde_json::Value`.
  </Step>
  <Step title="On failure">
    Retries validation failures up to `structured_output_retries` times with error feedback, then returns the completed main run with `RunResult.extraction_error`.
  </Step>
</Steps>

## Schema types

### `OutputSchema`

The primary schema type:

```rust
pub struct OutputSchema {
    pub schema: MeerkatSchema,
    pub name: Option<String>,
    pub strict: bool,
    pub compat: SchemaCompat,
    pub format: SchemaFormat,
}
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `schema` | `MeerkatSchema` | (required) | The JSON schema definition |
| `name` | `Option<String>` | `None` | Optional schema name (used by some providers) |
| `strict` | `bool` | `false` | Whether to request stricter provider-side lowering / constrained decoding behavior where supported |
| `compat` | `SchemaCompat` | `Lossy` | Compatibility mode for provider lowering |
| `format` | `SchemaFormat` | `MeerkatV1` | Schema format version |

#### Construction methods

```rust
// From a raw JSON value
let schema = OutputSchema::new(json!({
    "type": "object",
    "properties": {
        "summary": { "type": "string" },
        "score": { "type": "number" }
    },
    "required": ["summary", "score"]
}))?;

// From a JSON string (raw schema or wrapper format)
let schema = OutputSchema::from_json_str(r#"{"type": "object", ...}"#)?;

// From a JSON value (raw schema or wrapper format)
let schema = OutputSchema::from_json_value(value)?;

// From a Rust type via schemars
let schema = OutputSchema::from_type::<MyStruct>()?;
```

#### Builder methods

```rust
let schema = OutputSchema::new(json!({...}))?
    .with_name("analysis-result")
    .strict()
    .with_compat(SchemaCompat::Strict)
    .with_format(SchemaFormat::MeerkatV1);
```

<Accordion title="Wrapper format">
`OutputSchema` supports a wrapper format for explicit configuration. If the JSON object contains a `schema` key and either a `format: "meerkat_v1"` marker or only wrapper keys (`schema`, `name`, `strict`, `compat`, `format`), it is parsed as a wrapper:

```json
{
  "schema": {
    "type": "object",
    "properties": {
      "result": { "type": "string" }
    }
  },
  "name": "my-schema",
  "strict": true,
  "compat": "strict",
  "format": "meerkat_v1"
}
```

Otherwise, the entire JSON value is treated as a raw schema.
</Accordion>

### `MeerkatSchema`

Newtype around `serde_json::Value` with normalization:

```rust
pub struct MeerkatSchema(Value);
```

- Constructed via `MeerkatSchema::new(Value)` which applies `normalize_schema()`.
- **Normalization**: ensures all object-typed nodes have `properties` and `required` keys (inserting empty defaults if missing). This prevents provider-specific compilation issues.
- Returns `SchemaError::InvalidRoot` if the root is not a JSON object.

### `SchemaFormat`

Schema format versions:

| Variant | Description |
|---------|-------------|
| `MeerkatV1` | Current format version (default) |

### `SchemaCompat`

Compatibility mode for provider-specific schema lowering:

| Variant | Description |
|---------|-------------|
| `Lossy` | Best-effort lowering; unsupported features are dropped with warnings (default) |
| `Strict` | Reject schemas with unsupported features for the target provider |

### `SchemaWarning`

Warnings emitted during schema compilation:

```rust
pub struct SchemaWarning {
    pub provider: Provider,
    pub path: String,
    pub message: String,
}
```

### `CompiledSchema`

Provider-compiled schema output:

```rust
pub struct CompiledSchema {
    pub schema: Value,
    pub warnings: Vec<SchemaWarning>,
}
```

### `SchemaError`

Schema errors:

| Variant | Description |
|---------|-------------|
| `InvalidRoot` | Schema must be a JSON object at the root |
| `UnsupportedFeatures { provider, warnings }` | Schema uses features not supported by the target provider (only in `Strict` compat mode) |

## Extraction turn details

The extraction turn logic:

### Attempt flow

1. **Max attempts** = `structured_output_retries + 1` (default: 2 + 1 = 3 attempts).
2. **First attempt prompt**: `"Based on our conversation, provide the final output as valid JSON matching the required schema. Output ONLY the JSON, no additional text or markdown formatting."`
3. **Retry prompt** (on validation failure): `"The previous output was invalid: {error}. Please provide valid JSON matching the schema. Output ONLY the JSON, no additional text."`
4. LLM is called with **no tools** and **temperature 0.0**.
5. Response text is trimmed, then markdown code fences are stripped (handles `` ```json `` and `` ``` `` wrappers).
6. Parsed as JSON via `serde_json::from_str`.
7. Validated against the compiled schema via `jsonschema::Validator` when the `jsonschema` feature is enabled.

<Note>
If Meerkat is built without the `jsonschema` feature, structured output still parses JSON and returns it, but schema validation is skipped.
</Note>

### On success

Returns `RunResult` with:
- `text`: the committed main-turn assistant output
- `structured_output`: `Some(parsed_value)` -- the validated JSON
- `schema_warnings`: any warnings from schema compilation
- `turns`: includes extraction attempts in the count

### On failure

Returns `Ok(RunResult)` for the completed main run with `structured_output: None` and `extraction_error: Some(error)`:

```rust
pub struct ExtractionError {
    last_output: String,  // Main-turn output extraction attempted to transform
    attempts: u32,        // Extraction attempts made before failure
    reason: String,       // Extraction failure reason
}
```

Extraction failure can come from validation exhaustion, provider errors, hook or
budget denial, schema compilation failure, or tool-call-shaped output during the
post-main-turn extraction phase.

## Provider schema compilation

The `AgentLlmClient` trait includes a `compile_schema()` method:

```rust
fn compile_schema(&self, output_schema: &OutputSchema) -> Result<CompiledSchema, SchemaError>;
```

The default implementation passes through the normalized schema without provider-specific lowering. Provider adapters override this to apply transformations:

- **Anthropic**: may add `additionalProperties: false` to object nodes
- **Gemini**: may strip unsupported JSON Schema keywords
- **OpenAI**: may apply strict-mode transformations

Schema warnings are collected during compilation and included in `RunResult.schema_warnings`.

## Configuration

### Agent config fields

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `output_schema` | `Option<OutputSchema>` | `None` | JSON schema for extraction |
| `structured_output_retries` | `u32` | `2` | Max validation retries |

## Usage

<Tabs>
  <Tab title="CLI">
    ```bash
    # Inline JSON schema
    rkat run "Analyze this code" --schema '{"type":"object","properties":{"bugs":{"type":"array","items":{"type":"string"}}},"required":["bugs"]}'

    # Schema from file
    rkat run "Analyze this code" --schema schema.json
    ```

    The `--schema` flag accepts either a file path or inline JSON. The CLI detects files by checking if the value is an existing path.
  </Tab>
  <Tab title="JSON-RPC">
    Via `session/create` and `turn/start`:

    ```json
    {
      "jsonrpc": "2.0",
      "method": "session/create",
      "params": {
        "prompt": "Analyze this code",
        "output_schema": {
          "type": "object",
          "properties": {
            "bugs": { "type": "array", "items": { "type": "string" } }
          },
          "required": ["bugs"]
        },
        "structured_output_retries": 3
      }
    }
    ```
  </Tab>
  <Tab title="REST API">
    ```bash
    curl -X POST http://localhost:8080/sessions \
      -H "Content-Type: application/json" \
      -d '{
        "prompt": "Analyze this code",
        "output_schema": {
          "type": "object",
          "properties": {
            "bugs": { "type": "array", "items": { "type": "string" } }
          },
          "required": ["bugs"]
        }
      }'
    ```
  </Tab>
  <Tab title="MCP Server">
    The MCP server tool `meerkat_run` accepts `output_schema` as part of its input parameters.
  </Tab>
</Tabs>

## Wire parameters

Structured output is passed directly on the normal per-request session surfaces:

```rust
pub struct CreateSessionRequest {
    pub prompt: ContentInput,
    // ...
    pub build: Option<SessionBuildOptions>,
}

pub struct SessionBuildOptions {
    pub output_schema: Option<OutputSchema>,
    pub structured_output_retries: u32,
    // ...
}
```

## `RunResult` fields

When structured output extraction succeeds, the `RunResult` contains:

```rust
pub struct RunResult {
    pub text: String,                              // Main-turn assistant output
    pub session_id: SessionId,
    pub usage: Usage,
    pub turns: u32,                                // Includes extraction attempts
    pub tool_calls: u32,
    pub structured_output: Option<Value>,           // Validated JSON value
    pub extraction_error: Option<ExtractionError>,   // Extraction failure details
    pub schema_warnings: Option<Vec<SchemaWarning>>, // Compilation warnings
}
```

The `structured_output` field is `Some(value)` when extraction succeeds and `None` when no schema was configured or extraction failed. The `text` field remains the committed main-turn assistant output.

## SDK usage

### Basic structured output

```rust
use meerkat_core::OutputSchema;
use serde_json::json;

let schema = OutputSchema::new(json!({
    "type": "object",
    "properties": {
        "summary": { "type": "string" },
        "confidence": { "type": "number", "minimum": 0, "maximum": 1 }
    },
    "required": ["summary", "confidence"]
}))?;

// Configure via AgentBuildConfig
let mut build_config = AgentBuildConfig::new("claude-sonnet-4-6");
build_config.output_schema = Some(schema);
build_config.structured_output_retries = 3;
```

### Using `from_type` with schemars

```rust
use meerkat_core::OutputSchema;
use schemars::JsonSchema;
use serde::Deserialize;

#[derive(JsonSchema, Deserialize)]
struct AnalysisResult {
    summary: String,
    confidence: f64,
    issues: Vec<String>,
}

let schema = OutputSchema::from_type::<AnalysisResult>()?;
```

### Handling the result

```rust
let result: RunResult = agent.run("Analyze this code").await?;

if let Some(structured) = &result.structured_output {
    let analysis: AnalysisResult = serde_json::from_value(structured.clone())?;
    println!("Confidence: {}", analysis.confidence);
}

if let Some(warnings) = &result.schema_warnings {
    for w in warnings {
        eprintln!("Schema warning for {:?} at {}: {}", w.provider, w.path, w.message);
    }
}
```

## See also

- [Examples: Structured output](/examples/structured-output)
- [API reference](/reference/api-reference)
- [Rust overview](/rust/overview)
