# Instructor: when an LLM breaks the schema, send the error back and ask again

> Every client runs a different provider. Forward deployed engineers need schema enforcement that does not depend on any one vendor's API, and Pydantic with Instructor is built for that job.

Original: https://fdetimes.net/en/tools/instructor-pydantic-llm-schema-validation/

OpenAI's Structured Outputs documentation contains a sentence worth pausing on: JSON mode and Structured Outputs both guarantee valid JSON, but only Structured Outputs guarantees that the output matches the schema. So a JSON string that parses cleanly can still be missing fields, carry the wrong types, or put a meaningless date where an amount of money should be.

In a demo, that hardly matters. For an FDE connecting an LLM to a client's production systems, it is the failure that surfaces at two in the morning, halfway through a long batch, on a provider you did not choose.

Clients rarely let you choose. One uses OpenAI, another Anthropic, a third runs open models through Ollama on its internal network because the data cannot leave the company. You need schema enforcement that works on all of those stacks. Instructor supports a long list of providers, so it fits that problem well.

## Pydantic writes the contract, Instructor enforces it

According to its own documentation, Pydantic is the most widely used data validation library in Python, and its validation core is written in Rust. What matters more here is that a Pydantic model can export JSON Schema, which you can pass straight to an LLM API to say "return exactly this shape".

Instructor, written by Jason Liu, builds on that foundation. Its homepage describes it as a tool for extracting structured data from any LLM with type safety, validation and automatic retries. One line in the README sums up how it works: failed validations are automatically retried with the error message.

The phrase that matters is "with the error message". Instructor does not just keep calling until it gets lucky. It sends the error Pydantic raised back to the model, much as a reviewer returns a pull request with specific comments rather than simply rejecting it. The model learns where it went wrong and does not have to guess again from scratch.

## Turning the client's rules into code

Imagine you are working with a logistics company, and your task is to read order emails and push them into the warehouse system. Instead of writing a long prompt that says "remember to return JSON, remember quantity is an integer", you declare the contract in Pydantic:

```python
from pydantic import BaseModel, Field, field_validator

class OrderLine(BaseModel):
    sku: str = Field(description="Item code from the customer's catalog")
    quantity: int = Field(gt=0)
    unit: str

class Order(BaseModel):
    customer_code: str
    lines: list[OrderLine]
    requested_date: str

    @field_validator("customer_code")
    @classmethod
    def must_be_known(cls, v: str) -> str:
        if not v.startswith("KH-"):
            raise ValueError("customer_code must start with KH-")
        return v
```

(The field description reads "item code according to the client's catalogue", and the error message says "customer_code must start with KH-", where KH is short for *khách hàng*, Vietnamese for customer.)

You give this `Order` model to Instructor along with the email text. If the model returns `quantity` as the string "hai thùng" ("two cartons") or a `customer_code` without the prefix, Pydantic catches the error, Instructor packages it and asks again. What you end up with is a typed `Order` object that your IDE can autocomplete, with no `json.loads` and try/except anywhere in your code.

The interesting part is the `must_be_known` validator. It encodes the client's business knowledge, not the model's, and you have put it into the error-correction loop without retraining the model on anything.

This is why Instructor suits FDE work. Every client has its own rules, and Pydantic gives you a place to write them in code rather than in prompt wording.

## Retries are insurance, not magic

Every re-ask is another API call, which means more latency and more cost, and validators only catch what you bother to write. A loose schema full of `str` fields will pass through Instructor without a single re-ask, even if the data inside is nonsense. Output quality still starts with schema quality.

If the client uses only OpenAI, try native Structured Outputs first, because it guarantees schema adherence at the API level instead of fixing errors afterwards. Instructor's advantage lies elsewhere: it runs on OpenAI, Anthropic, Google, Vertex AI, Mistral, Ollama, llama-cpp-python, Cohere and LiteLLM, so a single codebase can follow you from client to client.

## Learn Pydantic first, Instructor second

Installation is just `pip install instructor`, and the project is MIT-licensed, so the tool itself is not the obstacle. The obstacle is that most developers use Pydantic as a data template, whereas here it is a specification language: `Field(description=...)` is a prompt, `gt=0` is a constraint, `field_validator` is a business rule.

Practise writing models whose generated JSON Schema would tell a stranger exactly what you want.

When applying for jobs, if the description mentions extracting structured data from LLMs, put this experience near the top of your CV. Describe the work concretely, for example: "moved an extraction pipeline from plain prompts to Pydantic schemas with business validators, running through Instructor on two different providers".

Language models will keep getting things wrong. Your job is not to hope they get it right, but to write a contract tight enough that every mistake is caught, and corrected, before it touches the client's data.

**Try this week:**

- Take an extraction task you currently handle with a plain prompt, rewrite it as a Pydantic model with at least one field_validator, run it through Instructor and count how many times the model gets re-asked.
- Call .model_json_schema() on that model and read the generated JSON Schema carefully. This is what the LLM sees: are the field descriptions clear?
- If your client uses OpenAI, run the same schema through native Structured Outputs and compare its reliability with Instructor's retry approach.

## Sources

- [Instructor - Multi-Language Library for Structured LLM Outputs | Python, TypeScript, Go, Ruby - Instructor](https://python.useinstructor.com/)

- [Instructor: Structured Outputs for LLMs (567-labs/instructor)](https://github.com/567-labs/instructor)

- [Pydantic Validation](https://pydantic.dev/docs/validation/latest/get-started/)

- [Structured model outputs](https://developers.openai.com/api/docs/guides/structured-outputs)
