Skip to main content

Instructor

Derive Macro Instructor 

Source
#[derive(Instructor)]
{
    // Attributes available to this derive:
    #[llm]
}
Expand description

Derive macro for implementing Instructor and SchemaType

This macro automatically implements the SchemaType trait for a struct or enum, generating a JSON Schema representation based on the Rust type.

§Nested Types and Schema Embedding

When you have nested structs or enums, they should also derive Instructor to ensure their full schema is embedded in the parent type. This produces complete JSON schemas that help LLMs generate correct structured output.

use rstructor::Instructor;
use serde::{Serialize, Deserialize};

// Parent type derives Instructor
#[derive(Instructor, Serialize, Deserialize)]
struct Parent {
    child: Child,  // Child's schema will be embedded
}

// Nested types should also derive Instructor for complete schema
#[derive(Instructor, Serialize, Deserialize)]
struct Child {
    name: String,
}

The schema embedding happens at compile time, avoiding any runtime overhead.

§Validation

To add custom validation, use the validate attribute with a function path:

use rstructor::{Instructor, RStructorError};
use serde::{Serialize, Deserialize};

#[derive(Instructor, Serialize, Deserialize)]
#[llm(validate = "validate_product")]
struct Product {
    name: String,
    price: f64,
}

fn validate_product(product: &Product) -> rstructor::Result<()> {
    if product.price <= 0.0 {
        return Err(RStructorError::ValidationError(
            "price must be positive".into()
        ));
    }
    Ok(())
}

The validation function is called automatically when the LLM response is deserialized.

§Examples

§Field-level attributes

use rstructor::Instructor;
use serde::{Serialize, Deserialize};

#[derive(Instructor, Serialize, Deserialize, Debug)]
struct Person {
    #[llm(description = "Full name of the person")]
    name: String,

    #[llm(description = "Age of the person in years", example = 30)]
    age: u32,

    #[llm(description = "List of skills", example = ["Programming", "Writing", "Design"])]
    skills: Vec<String>,
}

§Container-level attributes

You can add additional information to the struct or enum itself:

use rstructor::Instructor;
use serde::{Serialize, Deserialize};

#[derive(Instructor, Serialize, Deserialize, Debug)]
#[llm(description = "Represents a person with their basic information",
      title = "PersonDetail",
      examples = [
        ::serde_json::json!({"name": "John Doe", "age": 30}),
        ::serde_json::json!({"name": "Jane Smith", "age": 25})
      ])]
struct Person {
    #[llm(description = "Full name of the person")]
    name: String,

    #[llm(description = "Age of the person in years")]
    age: u32,
}

#[derive(Instructor, Serialize, Deserialize, Debug)]
#[llm(description = "Represents a person's role in an organization")]
#[serde(rename_all = "camelCase")]
struct Employee {
    first_name: String,
    last_name: String,
    employee_id: u32,
}

#[derive(Instructor, Serialize, Deserialize, Debug)]
#[llm(description = "Represents a person's role in an organization",
      examples = ["Manager", "Director"])]
enum Role {
    Employee,
    Manager,
    Director,
    Executive,
}

§Container Attributes

  • description: A description of the struct or enum
  • title: A custom title for the JSON Schema (defaults to the type name)
  • examples: Example instances of the struct or enum
  • validate: A quoted Rust path to a custom validation function

§Field and Variant Attributes

Fields accept description, example, and examples. Enum variants accept description. Field optionality is inferred from Option<T>; there is no optional attribute.

The llm namespace is checked strictly. Unknown attributes, malformed values, invalid validation paths, and unsupported tuple/unit structs produce errors at the relevant source span instead of being ignored.

§Serde Integration

Supported Serde name and skip metadata is interpreted from the deserialization side of the wire contract:

  • Respects rename, rename_all, and rename_all_fields
  • Uses deserialize = "..." when names differ by direction
  • Omits fields and variants marked skip or skip_deserializing
  • Keeps skip_serializing fields because they remain valid inputs

Supported case transformations include “lowercase”, “UPPERCASE”, “camelCase”, “PascalCase”, and “snake_case”. For example, with #[serde(rename_all = "camelCase")], user_id becomes userId.