Using Instructor for Schema-Constrained Outputs Across Providers
Use Instructor to extract validated Pydantic objects from many LLM providers with one response_model workflow and automatic reask on validation failure.
Summary
Patch or create a provider client with Instructor, pass a Pydantic response_model, and let the library parse, validate, and retry until the schema is satisfied or max_retries is exhausted.
Recipe
- Define the smallest Pydantic model that your downstream code needs.
- Install
instructorand create a client withinstructor.from_provider("provider/model")(or provider-specific helpers). - Call
client.create(response_model=YourModel, messages=[...], max_retries=N). - Put business rules in Pydantic validators so retries receive precise error text.
- Swap only the provider/model string when you change vendors; keep the schema stable.
- Log retry counts and terminal validation failures for cost and quality monitoring.
- Promote the same models into Pydantic AI later if the task grows into a multi-tool agent.
Working Example
import instructor
from pydantic import BaseModel, Field, field_validator
from enum import Enum
class Category(str, Enum):
billing = "billing"
technical = "technical"
other = "other"
class SupportIntent(BaseModel):
category: Category
urgency: int = Field(ge=1, le=5, description="1=low, 5=immediate")
summary: str = Field(min_length=10)
contact_email: str | None = None
@field_validator("summary")
@classmethod
def no_placeholders(cls, v: str) -> str:
if "TODO" in v.upper():
raise ValueError("summary must not contain TODO placeholders")
return v.strip()
# Same schema, different providers (verify model ids at build)
client = instructor.from_provider("openai/gpt-4o-mini")
# client = instructor.from_provider("anthropic/claude-3-5-sonnet-20240620")
# client = instructor.from_provider("google/gemini-2.5-flash")
# client = instructor.from_provider("ollama/llama3.2") # local
intent = client.create(
response_model=SupportIntent,
max_retries=3,
messages=[
{
"role": "user",
"content": (
"Classify this ticket: "
"I was double charged on invoice #4412 and need a refund today. "
"Email me at user@example.com."
),
}
],
)
print(intent.category, intent.urgency)
print(intent.model_dump())You get a SupportIntent instance or an error after retries.
You do not hand-parse JSON fences.
Deep Dive
What Instructor optimizes for
Instructor is a structured output client, not a full agent runtime.
Strengths:
- provider-wide API for schema extraction
- automatic validation + reask
- streaming helpers (
create_partial,create_iterable- verify names at build) - type inference for IDE completion
Use it for classification, document extraction, form fill, and "LLM as function" calls.
Use Pydantic AI when you need tools, multi-step loops, deps injection, and UI stream adapters.
Provider portability pattern
Keep three layers separate:
- Schema module - pure Pydantic models
- Client factory -
from_provider/ API keys / timeouts - Call sites - messages +
response_model+ retry policy
That separation lets you route cheap vs strong models without rewriting validators.
Retry semantics
On validation failure Instructor typically:
- catches the Pydantic error
- appends repair context for the model
- requests another completion
- repeats until success or
max_retries
This recovers many near-miss outputs (wrong enum casing, missing optional confusion, out-of-range ints). It does not fix systematically impossible schemas or missing source data.
Multi-object and partial streams
For lists of records, prefer iterable/streaming modes when processing long documents so you can persist rows incrementally.
For progressive UI, partial object streaming can show fields as they fill; still commit only after final validation.
Hooks and observability
Attach completion hooks for logging kwargs, errors, and latency. Export retry counts as metrics: high retries mean schema smell or weak model choice.
OpenRouter and gateways
You can point OpenAI-compatible clients at gateways (including OpenRouter) when Instructor supports that provider wiring. Keep model slugs and structured-output capabilities verified per route; not every upstream handles schema mode equally.
Gotchas
- Retries are not free. Cap
max_retriesand alert when average retries rise. - Provider feature gaps. Some local models need simpler schemas or prompted JSON modes.
- Overfitting demos. A schema that works on three clean examples can fail on real tickets.
- Email/URL validators. Strict format validators without recovery hints cause thrash; consider normalizing in validators.
- Mixing agent tools into Instructor. If you need tools, graduate to Pydantic AI rather than bolting a pseudo-loop.
- Secret leakage in messages. Extraction prompts often include raw documents; apply redaction policies.
- Async misuse. Use
async_client=True(or async APIs) in async apps; do not block the event loop with synccreate.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
| Instructor | Simple, multi-provider, retries | Not a full agent framework |
Pydantic AI output_type | Agent-native structure | Heavier for pure extract |
| Provider SDK structured outputs | First-party features | Provider lock-in |
| LangChain structured parsers | Ecosystem integrations | More abstraction layers |
| Manual JSON parse | Zero deps | High production failure rate |
FAQs
When should I pick Instructor over Pydantic AI?
Pick Instructor for single-call or few-call structured extraction across providers. Pick Pydantic AI when the workflow needs tools, multi-turn agent control, or AI SDK streaming adapters.
Does Instructor work offline with Ollama?
Yes for many local models via the Ollama integration. Keep schemas smaller and test retries; local models vary widely on structured fidelity.
How do I share one schema with an agent later?
Put models in a shared package. Use them as Instructor response_model now and Pydantic AI output_type later without translation layers.
What belongs in messages vs Field descriptions?
Messages carry the instance data and task. Field descriptions and validators carry durable contract rules that should apply to every call.
Can I return nested lists of models?
Yes. Nested Pydantic models and lists are core use cases. Watch token size and consider chunking huge documents.
How do I handle documents longer than the context window?
Chunk with overlap, extract per chunk into list models, then merge with deterministic code or a second structured pass.
Is from_provider required?
It is the unified entry point in current docs. Older code may use from_openai style helpers; verify which API your installed version exposes.
How do I test without calling a model?
Unit-test validators with raw dicts via Model.model_validate. Keep a small live suite for provider-schema compatibility.
What if all retries fail?
Catch the error, route to a human queue or fallback model, and store the invalid raw completion for evals. Do not silently coerce.
Can Instructor classify and extract in one call?
Yes. Put category enums and extracted entities on one model when they share the same source text and trust boundary.
Related
- Pydantic AI & Instructor Basics - side-by-side intro
- Defining Agent Output Schemas with Pydantic Models - schema design recipe
- Handling Validation Failures and Retry Loops Gracefully - retry policy
- Why Structured Output Matters More as Agents Get Autonomous - conceptual stakes
- Pydantic AI & Instructor Best Practices - checklist
- Type-Safe Tool Definitions with Pydantic AI - when you outgrow pure extraction
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.