Structured Output
Rig can hand you a Rust value instead of a string. You name a target type, Rig sends its JSON Schema to the model, and the reply is deserialized into that type. There are two ways to do it:
- an
Extractorturns a piece of text into aT. Use it when extraction is the whole job. agent.prompt_typed::<T>(..)runs an ordinary agent prompt (tools, history, hooks) and parses the final answer asT. Use it when structured output is one step of a larger agent workflow.
Minimal example
Section titled “Minimal example”The target type derives serde::Deserialize, serde::Serialize, and schemars::JsonSchema. Build
an extractor for it with ExtractorBuilder::<T>::new(model), then call extract:
use rig::extractor::ExtractorBuilder;use rig::providers::openai::{self, OpenAI};use schemars::JsonSchema;use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, JsonSchema)]struct Person { name: Option<String>, age: Option<u8>, profession: Option<String>,}
#[tokio::main]async fn main() -> anyhow::Result<()> { let model = OpenAI::from_env()?.completion(openai::GPT_5_5); let extractor = ExtractorBuilder::<Person>::new(model).build();
let person = extractor .extract("John Doe is a 30 year old doctor.") .await? .output;
println!("{person:?}"); Ok(())}Person { name: Some("John Doe"), age: Some(30), profession: Some("doctor") }extract(..).await returns a
TypedPromptResponse<T>: the value
is in .output, and .usage holds the tokens spent, summed over every attempt.
How it works
Section titled “How it works”An extractor is an agent with a fixed preamble and one tool, submit, whose
parameters are your type’s JSON Schema. Each extraction is a single model turn with tool choice set
to required, so the model must call submit; Rig deserializes the call’s arguments into T. If the
model answers with plain JSON that already matches T instead of calling the tool, Rig accepts that
too.
Configuring an extractor
Section titled “Configuring an extractor”ExtractorBuilder exposes the agent settings that make sense for extraction:
let extractor = ExtractorBuilder::<Person>::new(model) .append_preamble("Ages are given in years; ignore honorifics like 'Dr.'") .context("Doctors in this dataset are always medical doctors.") .max_tokens(512) .retries(2) .build();append_preambleadds instructions after the built-in extraction preamble (which tells the model to callsubmit; it can’t be replaced).contextadds a static document;dynamic_context(n, index)retrievesndocuments from a vector index for every extraction.retries(n)re-runs a failed attempt up tonmore times. An attempt fails when the request errors, the model doesn’t submit anything, or the submitted JSON doesn’t fitT.additional_paramsandadd_hookwork as they do onAgentBuilder.
extract(..) returns a run you can adjust before awaiting it, for example
.history(&messages) to extract from a conversation, or .retries(n) for one call.
Typed prompts on an agent
Section titled “Typed prompts on an agent”When the agent already exists and you want its final answer as a value, use prompt_typed. It sets
T’s schema as the run’s structured-output schema (providers with native structured output enforce
it) and parses the final text as T. Tools, history, hooks and max_turns work as in any other
run:
#[derive(Debug, Deserialize, schemars::JsonSchema)]struct Forecast { city: String, temperature_c: f64,}
let forecast: Forecast = agent .prompt_typed::<Forecast>("Make up a plausible forecast for Paris.") .max_turns(3) .retries(1) .await? .output;To give every run of an agent a fixed schema instead, set it on the builder with
AgentBuilder::output_schema::<T>() and parse the reply yourself.
Error handling
Section titled “Error handling”Extractions and typed prompts fail with
StructuredOutputError:
EmptyResponse: the model produced nothing to parse (for an extractor: it never calledsubmit).Deserialization { output, error }: the model’s JSON didn’t fitT.outputholds the raw text.Prompt(PromptError): the run itself failed, for example a provider error or the turn budget running out. See Error handling.
use rig::completion::StructuredOutputError;
match extractor.extract("...").await { Ok(response) => { /* use response.output */ } Err(StructuredOutputError::EmptyResponse) => { eprintln!("the model did not produce structured data"); } Err(StructuredOutputError::Deserialization { output, error }) => { eprintln!("unparseable output ({error}): {output}"); } Err(err) => return Err(err.into()),}Many inputs
Section titled “Many inputs”Build an extractor once and reuse it: it is cheap to call repeatedly and can be shared by reference
across concurrent tasks. Run extractions concurrently with futures, capping how many are in
flight:
use futures::{StreamExt, TryStreamExt};use rig::loaders::FileLoader;
let docs: Vec<String> = FileLoader::with_glob("notes/*.txt")? .read() .ignore_errors() .into_iter() .collect();
let people: Vec<Person> = futures::stream::iter(&docs) .map(|doc| async { extractor.extract(doc.as_str()).await.map(|r| r.output) }) .buffered(4) .try_collect() .await?;Document loaders cover reading files; the
multi_extract example runs
several extractors over the same inputs in parallel.
