# Structured output

> Ask a model for a Rust type with structured_prompt::<T>, where T derives schemars JsonSchema, and use DynStructuredPrompt when you need a trait object.

By the end of this page, a model call returns your own Rust struct. The provider shapes its answer to the struct's JavaScript Object Notation (JSON) Schema.

Bring the types and traits into scope:

```rust
use schemars::{JsonSchema, schema_for};
use serde::Deserialize;
use swiftide::{
    agents::{Agent, StopReason, tools::control::StopWithArgs},
    integrations::openai::OpenAI,
    traits::{DynStructuredPrompt, StructuredPrompt as _},
};
```

Source: [`snippets/src/models/structured_output.rs#L4-L10`](https://github.com/bosun-ai/swiftide-website/blob/master/snippets/src/models/structured_output.rs#L4-L10)

Describe the answer as a type:

```rust
/// What a model reads out of a changelog.
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ReleaseSummary {
    /// One line that names the release theme.
    pub title: String,
    /// Changes a user notices.
    pub highlights: Vec<String>,
    /// True when upgrading needs code changes.
    pub breaking: bool,
}
```

Source: [`snippets/src/models/structured_output.rs#L14-L24`](https://github.com/bosun-ai/swiftide-website/blob/master/snippets/src/models/structured_output.rs#L14-L24)

Then ask the model for it:

```rust
let openai = OpenAI::builder()
    .default_prompt_model("gpt-5-mini")
    .build()?;

let summary = openai
    .structured_prompt::<ReleaseSummary>(
        format!("Summarise this changelog:\n{changelog}").into(),
    )
    .await?;

println!("{}: {} highlights", summary.title, summary.highlights.len());
if summary.breaking {
    println!("This release needs code changes.");
}
```

Source: [`snippets/src/models/structured_output.rs#L30-L43`](https://github.com/bosun-ai/swiftide-website/blob/master/snippets/src/models/structured_output.rs#L30-L43)

## Schema and deserialization

The doc comments on the fields become descriptions in the schema, so the model reads them as instructions.

`structured_prompt::<T>` is a method of the `StructuredPrompt` trait, so the trait must be in scope. The method takes a `Prompt`, so `.into()` turns a `String` or a `&'static str` into one. Templates and template context work as on [Prompts and templates](/models/prompts-and-templates/).

Swiftide builds the schema with `schemars::schema_for!(T)` and sends it with the rendered prompt. It then deserializes the JSON the model returns into `T`. When the JSON does not fit `T`, the call returns `LanguageModelError::PermanentError`.

You need schemars, serde and serde_json in your own crate:

```sh
cargo add swiftide@0.34 --features openai
cargo add schemars@1 serde serde_json --features serde/derive
```

`structured_prompt` itself needs no extra feature flag. The `json-schema` feature only derives `JsonSchema` on some of Swiftide's own types.

### Schemas for Swiftide types

With `json-schema`, `ToolCall`, `ToolSpec`, `ToolFeedback`, `StopReason` and the agent `State` derive `JsonSchema`. Your own types can then hold them as fields and still derive a schema, for example the request body of an approval endpoint:

```sh
cargo add swiftide@0.34 --features openai,swiftide-agents,json-schema
```

```rust
/// The body your approval endpoint accepts; the `json-schema` feature flag lets it derive
/// `JsonSchema` with Swiftide types as fields.
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ApprovalDecision {
    /// The call the agent stopped on, as `StopReason::FeedbackRequired` reported it.
    pub tool_call: swiftide::chat_completion::ToolCall,
    /// The person's decision.
    pub feedback: swiftide::traits::ToolFeedback,
}

/// Prints the JSON Schema of the approval body, for example for an OpenAPI document.
pub fn approval_schema() -> anyhow::Result<()> {
    let schema = schema_for!(ApprovalDecision);
    println!("{}", serde_json::to_string_pretty(&schema)?);
    Ok(())
}
```

Source: [`snippets/src/models/structured_output.rs#L88-L104`](https://github.com/bosun-ai/swiftide-website/blob/master/snippets/src/models/structured_output.rs#L88-L104)

`StopReason` and `State` live in `swiftide::agents`, so they also need the `swiftide-agents` feature flag. The `json-schema` feature adds no API of its own.

## Schema rules

OpenAI clients send the schema in strict mode. This holds for Chat Completions and for the Responses application programming interface (API). Strict mode has two rules for every object in the schema.

The first rule is `additionalProperties: false`. Add `#[serde(deny_unknown_fields)]` to the struct, and to every struct it contains. Schemars then writes `additionalProperties: false` for that object. Without it, OpenAI rejects the request with an error about `additionalProperties`.

The second rule is that every field is listed in `required`. Schemars leaves `Option<T>` fields and `#[serde(default)]` fields out of `required`, so OpenAI rejects them. Use a required field with a sentinel value, or an enum, instead.

Swiftide passes the schemars schema through as it is. It does not rewrite the schema to fit a provider, so the provider's rules on schemas apply to your type directly.

## Trait objects

`StructuredPrompt` is generic over `T`, so it is not dyn safe. You cannot store a `Box<dyn StructuredPrompt>`. Use `DynStructuredPrompt` instead:

```rust
let llm: Box<dyn DynStructuredPrompt> = Box::new(openai);

let value = llm
    .structured_prompt_dyn(
        "Summarise the 0.33 release.".into(),
        schema_for!(ReleaseSummary),
    )
    .await?;
let summary: ReleaseSummary = serde_json::from_value(value)?;
```

Source: [`snippets/src/models/structured_output.rs#L51-L59`](https://github.com/bosun-ai/swiftide-website/blob/master/snippets/src/models/structured_output.rs#L51-L59)

`structured_prompt_dyn` takes a `schemars::Schema` and returns a `serde_json::Value`. You deserialize the value yourself.

Every `DynStructuredPrompt` gets `StructuredPrompt` through a blanket implementation. To add structured output to your own client, implement `DynStructuredPrompt` only.

## Provider support

| Client                                                | Feature flag                                           | How the schema is sent                                                                                                |
| ----------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| `OpenAI`                                              | `openai`                                               | Chat Completions `response_format` with a JSON Schema. With `use_responses_api(true)`, the Responses API text format. |
| `Ollama`, `Groq`, `Gemini`, `OpenRouter`, `Dashscope` | `ollama`, `groq`, `gemini`, `open-router`, `dashscope` | Same as `OpenAI`.                                                                                                     |
| `AwsBedrock`                                          | `aws-bedrock`                                          | Converse `output_config` with a JSON Schema.                                                                          |
| `Anthropic`                                           | `anthropic`                                            | Not supported.                                                                                                        |
| `FastEmbed`                                           | `fastembed`                                            | Not supported.                                                                                                        |

Whether a JSON Schema response format works on the `GenericOpenAI` aliases depends on the endpoint and the model.

A structured call is not retried. [Errors, backoff, usage and tokens](/models/errors-backoff-and-usage/) explains why.

## Agents

An agent talks to its model through chat completion, not through `structured_prompt`. To get a typed final answer from an agent, replace the default stop tool with `StopWithArgs`. Agents live behind the `swiftide-agents` feature flag:

```sh
cargo add swiftide@0.34 --features openai,swiftide-agents
```

```rust
let mut agent = Agent::builder()
    .llm(&openai)
    .without_default_stop_tool()
    .tools([StopWithArgs::with_parameters_schema(schema_for!(
        ReleaseSummary
    ))])
    .build()?;

agent.query("Summarise the 0.33 release.").await?;

let summary: Option<ReleaseSummary> = match agent.stop_reason() {
    Some(StopReason::RequestedByTool(_, Some(payload))) => {
        Some(serde_json::from_value(payload.clone())?)
    }
    _ => None,
};
```

Source: [`snippets/src/models/structured_output.rs#L67-L82`](https://github.com/bosun-ai/swiftide-website/blob/master/snippets/src/models/structured_output.rs#L67-L82)

The model ends the run by calling `stop` with arguments that follow the `ReleaseSummary` schema. The stop reason is then `StopReason::RequestedByTool(_, Some(payload))`, where `payload` is the arguments as a `serde_json::Value`. `serde_json::from_value` turns it into your type.

The stop tool's schema follows different rules than `structured_prompt`. Swiftide normalizes tool schemas for OpenAI: it fills in `required` and adds `additionalProperties: false`. [Stopping and structured results](/agents/stopping-and-structured-results/) covers stop tools.

Tool arguments use schemars schemas too, so the same derive works for a tool's arguments; see [Tools](/agents/tools/).

## Reference

- [`StructuredPrompt`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.StructuredPrompt.html)
- [`DynStructuredPrompt`](https://docs.rs/swiftide/0.34/swiftide/traits/trait.DynStructuredPrompt.html)
- [`Prompt`](https://docs.rs/swiftide/0.34/swiftide/prompt/struct.Prompt.html)
- [`GenericOpenAI`](https://docs.rs/swiftide/0.34/swiftide/integrations/openai/struct.GenericOpenAI.html)
- [`AwsBedrock`](https://docs.rs/swiftide/0.34/swiftide/integrations/aws_bedrock_v2/struct.AwsBedrock.html)
- [`LanguageModelWithBackOff`](https://docs.rs/swiftide/0.34/swiftide/indexing/struct.LanguageModelWithBackOff.html)
- [`Agent`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.Agent.html)
- [`AgentBuilder`](https://docs.rs/swiftide/0.34/swiftide/agents/struct.AgentBuilder.html)
- [`StopWithArgs`](https://docs.rs/swiftide/0.34/swiftide/agents/tools/control/struct.StopWithArgs.html)
- [`StopReason`](https://docs.rs/swiftide/0.34/swiftide/agents/enum.StopReason.html)
- [`LanguageModelError`](https://docs.rs/swiftide/0.34/swiftide/chat_completion/errors/enum.LanguageModelError.html)

## Next

[Images and multimodal input](/models/images/)