Want Stable, Structured LLM Outputs? Try Instructor!

Daichi Kugimiya

2025.10.2

This blog post is a translation of a Japanese article posted on May 8th, 2025.

Introduction: The Challenge of Controlling LLM Output

Have you ever thought something like this when developing an AI app?

In developing applications using generative AI, I’ve found that one of the key challenges is consistently obtaining structured data from LLMs.

Traditionally, even if you prompted the AI to “please respond in JSON format”, the response you got back wasn’t always what you expected.

Some common problems:

  • A string that should end with "description": "Good for your skin" ends with ...Good for your skin", with an extra comma.
  • A required key (e.g. rating ) is missing.
  • Getting a string ("5") where we would expect a number (5)

To handle these incomplete responses, a lot of error-handling code was necessary. I’m sure many of you have struggled with handling edge cases like “What if this key is missing?” or “What if parsing fails?”

That’s where instructor-js comes in. Today, I’ll introduce this library that allows you to handle LLM outputs in a type-safe manner using TypeScript and Zod. This library makes it easy to get structured and reliable responses.

What is instructor-js?

In short, instructor-js is a library that ensures the OpenAI API response conforms to the type you define with Zod.

The magic behind it lies in OpenAI’s Tool Calling feature. instructor converts the schema we define with Zod into the Tool Calling format and instructs the LLM, “Please use this tool (schema) to answer.” This forces the LLM to follow the specified structure, and as a result, we can reliably get the data in the format we want.

A Practical Example: Fetching Hot Spring Recommendations

Seeing is believing. Let’s use instructor to ask an LLM, “Give me three recommendations for hot springs” and receive that information in a clean array.

1. Setup: Installing and Configuring the Library

First, let’s install the necessary libraries.

npm install openai zod instructor-js

Next, initialize the OpenAI client and “patch” it with instructor to extend it (this is the standard way of using instructor).

import OpenAI from "openai";
import Instructor from "instructor-js";

// Initialize the standard OpenAI client
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// Extend the client with instructor!
export const instructor = Instructor({
  client: openai,
  mode: "TOOLS", // Specify Tool Calling mode
});

2. Type Definition: Defining the Desired Response Shape with Zod

Next, we define the type of data we want the LLM to return using Zod. This time, we’ll define a type for individual hot spring information and a type for the overall response that contains an array of them.

import { z } from "zod";

// Schema for a single hot spring's information
const OnsenSchema = z.object({
  name: z.string().describe("The name of the hot spring"),
  prefecture: z.string().describe("The prefecture where the hot spring is located"),
  features: z
    .array(z.string())
    .describe("An array of short keywords describing the features of the hot spring"),
  rating: z
    .number()
    .min(1)
    .max(5)
    .describe("A recommendation rating from 1 to 5"),
});

// Schema for the entire response
export const OnsenResponseSchema = z.object({
  onsenList: z.array(OnsenSchema).describe("A list of recommended hot springs"),
});

// We can also infer the TypeScript type
export type OnsenResponse = z.infer<typeof OnsenResponseSchema>;

By adding .describe(), we help the LLM better understand the meaning of each field, making it more likely to generate the expected data.

3. Execution: Calling the API with instructor

We’re all set. Let’s actually call the API using instructor.

import { instructor } from "./lib/openai";
import { OnsenResponseSchema, OnsenResponse } from "./schemas";

async function getOnsenRecommendations(): Promise<OnsenResponse> {
  console.log("Fetching recommended hot spring information...");

  const response = await instructor.chat.completions.create({
    model: "gpt-4o",
    messages: [
      {
        role: "user",
        content: "Please give me three recommendations for hot springs.",
      },
    ],
    response_model: {
      schema: OnsenResponseSchema,
      name: "OnsenRecommendations",
    },
    max_retries: 3, // This is important!
  });

  return response;
}

getOnsenRecommendations().then((data) => {
  console.log("Successfully fetched!");
  console.log(JSON.stringify(data, null, 2));

  // The type is guaranteed, so we can access properties directly!
  console.log("\\\\n--- Recommendation #1 ---");
  console.log(`Name: ${data.onsenList[0].name}`);
  console.log(`Location: ${data.onsenList[0].prefecture}`);
  console.log(`Rating: ★${data.onsenList[0].rating}`);
});

All you need to do is specify the Zod schema you defined earlier in response_model and set max_retries.

Example of execution result:

{
  "onsenList": [
    {
      "name": "Hakone Yuryo",
      "prefecture": "Kanagawa Prefecture",
      "features": [
        "Available for day trips",
        "Old folk house-style atmosphere",
        "19 private open-air baths for rent"
      ],
      "rating": 5
    },
    {
      "name": "Tenzan Tohji-kyo",
      "prefecture": "Kanagawa Prefecture",
      "features": [
        "Rustic open-air baths",
        "Free-flowing spring water from its own source",
        "Ample rest areas"
      ],
      "rating": 4
    },
    {
      "name": "Ryuguden Honkan",
      "prefecture": "Kanagawa Prefecture",
      "features": [
        "Spectacular views of Lake Ashi and Mt. Fuji",
        "Registered Tangible Cultural Property of Japan",
        "Day-trip bathing plans available"
      ],
      "rating": 5
    }
  ]
}

--- Recommendation #1 ---
Name: Hakone Yuryo
Location: Kanagawa Prefecture
Rating: ★5

Look at that! We were able to get a perfectly typed object without writing any type conversion or parsing code. The developer experience is great, with editor autocompletion working for things like data.onsenList[0].name.

The True Power of instructor: The Automatic Retry Feature

I believe the true value of this library goes beyond simple type conversion.

What’s particularly noteworthy is the automatic retry feature set with max_retries. This is not just a simple re-execution.

Suppose the LLM forgets the rating field on the first attempt. Then…

  1. instructor receives the response.
  2. Zod throws a validation error saying, “The rating field is required but does not exist!”
  3. instructor catches that error.
  4. instructor automatically sends a second request to the LLM with an additional instruction: “The previous response failed with this error. Next time, be sure to include rating in your response.”

This Self-Correction loop frees developers from implementing complex validation. The code remains clean, and the system’s reliability improves.

Conclusion

instructor-js is a powerful library that transforms the “uncertain” responses from an LLM into “certain, typed data.”

  • Type Safety: Ensures type safety at both compile-time and runtime with Zod schemas.
  • Automatic Retries: Achieves reliable output with its self-correction feature on failure.
  • Development Efficiency: Eliminates the need for complex error handling, allowing you to focus on business logic.

Some key benefits:

If you’re facing challenges with obtaining structured data in your LLM-powered development, I highly recommend giving instructor-js a try. it will significantly improve your development experience.

Reference Links

採用情報

Blog一覧へ戻る

Feel free to contact us

We provide support
for all aspects of SRE,
from design and technical support
to deployment of tools for SRE operations.

Brochure request and inquiries