rstructor: Structured LLM Outputs for Rust
Get structured, validated data out of any LLM as native Rust structs and enums. Define the shape you want as plain Rust types — rstructor generates the JSON Schema, prompts the model, parses the response, and retries on validation errors until the data fits.
Features
- Type-safe schemas from Rust types — Derive
Instructoron structs and enums; rstructor generates the JSON Schema and validated parser for you, no hand-written prompts or DTOs - Multi-provider, one API — OpenAI, Anthropic, Grok (xAI), and Gemini behind a single
materialize()call with swappable clients - Validation with automatic re-ask — Built-in type checking plus custom business rules; validation failures are fed back to the model and retried until the data is correct
- Rich, nested data — Nested objects, arrays, optionals, maps, and enums with associated data, with validation that recurses through the whole tree
- Familiar if you know Pydantic + Instructor — The same structured-output workflow as Python's Instructor + Pydantic, with Rust's compile-time type safety
Installation
[]
= "0.3"
= { = "1.0", = ["derive"] }
= { = "1.0", = ["rt-multi-thread", "macros"] }
Quick Start
Describe the shape you want as plain Rust types, then turn a line of free-form text into a fully-typed, validated value:
use ;
use ;
async
Every field is inferred, not transcribed: the urgency is read from the tone and deadline, the email is plucked out of mid-sentence text, and the tags are synthesized — all parsed into the exact types you declared.
Request Builder
materialize, generate, and (with the tools feature) tool run are also
available through a fluent builder that attaches context, images, and tools to a
single request. Bring RequestExt into scope and chain the pieces you need:
use ;
let client = from_env?;
// Add context that is prepended to the prompt, then materialize a struct.
let movie: Movie = client
.with_system
.materialize
.await?;
// Or start from `.request()` and combine builders before a terminal.
let summary = client
.request
.system
.generate
.await?;
The terminals are materialize::<T>(prompt) (structured), generate(prompt)
(text), and — with the tools feature — run(prompt) (text, calling any
attached tools in a loop). Builders compose: with_system, with_media, and
with_tools can be chained in any order before the terminal.
Providers
use ;
// OpenAI (reads OPENAI_API_KEY)
let client = from_env?.model;
// Anthropic (reads ANTHROPIC_API_KEY)
let client = from_env?.model;
// Grok/xAI (reads XAI_API_KEY)
let client = from_env?.model;
// Gemini (reads GEMINI_API_KEY)
let client = from_env?.model;
// Custom endpoint (local LLMs, proxies)
let client = new?
.base_url
.model;
Selecting a provider at runtime
LLMClient::materialize is generic, so the trait isn't object-safe (Box<dyn LLMClient> is impossible). Use AnyClient when the provider is decided at runtime (CLI flag, config, env) and you want to store it in a single type:
use ;
// Pick a provider dynamically, reading its key from the environment.
let provider = Anthropic; // e.g. parsed from a config file
let client = from_env_for?;
let movie: Movie = client.materialize.await?;
// Or auto-detect from whichever API key is set:
let client = from_env?;
// Or wrap a pre-configured client:
let client: AnyClient = from_env?.model.into;
Validation
Add custom validation with automatic retry on failure:
use ;
// Retries are enabled by default (3 attempts with error feedback)
// To increase retries:
let client = from_env?.max_retries;
// To disable retries:
let client = from_env?.no_retries;
Complex Types
Nested Structures
Enums with Data
Serde Rename Support
rstructor respects #[serde(rename)] and #[serde(rename_all)] attributes:
Supported case conversions: lowercase, UPPERCASE, camelCase, PascalCase, snake_case, SCREAMING_SNAKE_CASE, kebab-case, SCREAMING-KEBAB-CASE.
Dates, UUIDs, and Custom Types
use ;
use Instructor;
use ;
use Uuid;
For your own domain-specific scalar types, implement CustomTypeSchema plus SchemaType:
use CustomTypeSchema;
use ;
use ;
;
Multimodal (Image Input)
Analyze images with structured extraction across all major providers by
attaching media to a request with with_media:
use ;
async
MediaFile::new(uri, mime_type) is also available for URL/URI-based media input.
The lower-level LLMClient::materialize_with_media(prompt, &media) method does
the same thing in one call when you do not need the builder.
Provider examples:
cargo run --example openai_multimodal_example --features openaicargo run --example anthropic_multimodal_example --features anthropiccargo run --example grok_multimodal_example --features grokcargo run --example gemini_multimodal_example --features gemini
Extended Thinking
Configure reasoning depth for supported models:
use ThinkingLevel;
// GPT-5.5, Claude 4.6 Sonnet, Gemini 3.1
let client = from_env?
.model
.thinking_level;
// Levels: Off, Minimal, Low, Medium, High
Token Usage
let result = client..await?;
println!;
if let Some = result.usage
Error Handling
use ;
match client..await
Streaming
Enable the streaming feature to stream responses as they are generated.
= { = "0.3", = ["streaming"] }
materialize_iter streams a list of structured objects, yielding each item as soon as it is fully generated and validated — the common case where you want a long list without buffering the whole response:
use StreamExt;
use ;
let client = from_env?;
let mut stream = client.;
while let Some = stream.next.await
generate_stream streams raw text deltas:
let mut stream = client.generate_stream;
while let Some = stream.next.await
There is also materialize_stream, which streams a single object as progressive StreamedObject::Partial(json) snapshots followed by a validated Complete(T).
All are available on every provider (OpenAI, Anthropic, Grok, Gemini). See examples/streaming_example.rs.
Tool Calling
Enable the tools feature to let the model call your typed Rust functions and feed the results back, looping until it produces a final answer. Tool argument types derive Instructor, so their JSON Schema is generated automatically.
= { = "0.3", = ["tools"] }
use ;
use ;
use json;
let toolbox = new.with;
let client = from_env?;
let answer = client
.with_tools
.system // optional
.run
.await?;
Works with all providers (OpenAI, Anthropic, Grok, Gemini). See examples/tool_calling_example.rs.
Testing (offline)
Enable the mock feature to unit-test code that extracts structured data without any
network or API key. MockClient implements LLMClient, so it drops into any
C: LLMClient slot; scripted responses flow through the real deserialize +
validate() path, so you can test schema/validation failures, not just happy paths.
[]
= { = "0.3", = ["mock"] }
use ;
use ;
// Your code under test is generic over the client:
async
async
Script multiple responses with with_response/with_responses (a FIFO queue), branch
on the request with with_responder, simulate the validation re-ask loop with
with_retries, attach token usage with with_usage, and assert on captured requests via
requests() / last_request(). The mock feature pulls in no extra dependencies and
works even in a schema-only build; streaming and tool-loop mocking light up when the
streaming / tools features are also enabled. See examples/mock_testing_example.rs.
Feature Flags
[]
= { = "0.3", = ["openai", "anthropic", "grok", "gemini"] }
openai,anthropic,grok,gemini— Provider backends (each pulls in the shared HTTP/tokiostack)derive— Derive macro (default)logging— Tracing integrationstreaming— Streaming viagenerate_stream/materialize_iter/materialize_stream(opt-in)tools— Tool/function calling viaToolbox+client.with_tools(..).run(..)(opt-in)mock—MockClientfor offline unit testing (opt-in; see Testing)
All features are on by default. For a schema-only build — generate JSON Schema from your types with no networking, tokio, or reqwest — disable the providers:
[]
= { = "0.3", = false, = ["derive"] }
This keeps the derive macro, SchemaType, the Instructor trait, and the LLMClient trait (so you can implement your own backend) without the async/HTTP dependency tree.
Examples
See examples/ for complete working examples:
For Python Developers
If you're coming from Python and searching for:
- "pydantic rust" or "rust pydantic" — rstructor provides similar schema validation and type safety
- "instructor rust" or "rust instructor" — same structured LLM output extraction pattern
- "structured output rust" or "llm structured output" — exactly what rstructor does
- "type-safe llm rust" — ensures type safety from LLM responses to Rust structs
License
MIT — see LICENSE