Skip to content

Structured Output with Parse Retries

Arian Amiramjadi edited this page Dec 24, 2025 · 1 revision

Structured Output with Parse Retries

Instructor-style structured output extraction with automatic retry on parse errors. When parsing fails, the library tells the model what went wrong and asks for correction.

Quick Start

type Person struct {
    Name    string `json:"name"`
    Age     int    `json:"age"`
    Email   string `json:"email"`
}

var person Person
err := ai.Claude().
    IntoWithRetry("Extract person info: John Smith, 32, [email protected]", &person, 3)

fmt.Printf("%+v\n", person)
// {Name:John Smith Age:32 Email:[email protected]}

Basic Parsing

Into (No Retry)

The original Into method parses without retries:

var result MyStruct
err := ai.Claude().Into("Extract from this text...", &result)

IntoWithRetry

Parse with automatic retry on failure. The model receives error feedback:

var result MyStruct
err := ai.Claude().IntoWithRetry("Extract from this text...", &result, 3)
// Retries up to 3 times if parsing fails

MustInto

Parse or panic (useful for scripts and tests):

var result MyStruct
ai.Claude().MustInto("Extract from this text...", &result)

How Retry Works

When parsing fails, the library:

  1. Sends the failed response back to the model
  2. Explains what went wrong (JSON syntax error, missing field, wrong type)
  3. Asks the model to fix and retry
  4. Repeats until success or max retries exhausted
Attempt 1: Model returns invalid JSON
→ "Your response had an error: unexpected end of JSON input"
→ "Please fix the JSON and try again"

Attempt 2: Model returns valid JSON but missing field
→ "Your response had an error: field 'email' is required"
→ "Please fix the JSON and try again"

Attempt 3: Success!

Generic Extractors

Extract

Type-safe extraction with generics:

type Product struct {
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

product, err := ai.Extract[Product](ai.Claude(), "iPhone 15 costs $999")

ExtractWithRetry

With custom retry count:

product, err := ai.ExtractWithRetry[Product](ai.Claude(), "iPhone 15 costs $999", 5)

ExtractList

Extract multiple items:

products, err := ai.ExtractList[Product](ai.Claude(), 
    "Products: iPhone $999, MacBook $1299, AirPods $249")

for _, p := range products {
    fmt.Printf("%s: $%.2f\n", p.Name, p.Price)
}

Struct Validation

Implement StructValidator

Add custom validation logic to your structs:

type Order struct {
    ID       string  `json:"id"`
    Amount   float64 `json:"amount"`
    Currency string  `json:"currency"`
}

// Implement the StructValidator interface
func (o *Order) Validate() error {
    if o.Amount <= 0 {
        return fmt.Errorf("amount must be positive")
    }
    if o.Currency == "" {
        return fmt.Errorf("currency is required")
    }
    if len(o.Currency) != 3 {
        return fmt.Errorf("currency must be 3 characters (e.g., USD)")
    }
    return nil
}

// Validation runs automatically after parsing
var order Order
err := ai.Claude().IntoWithRetry("Order #123 for $50", &order, 3)
// If validation fails, model is asked to fix the output

Validation Helpers

Built-in validation functions:

func (o *Order) Validate() error {
    // Check required fields
    if err := ai.ValidateRequired(o, "ID", "Amount", "Currency"); err != nil {
        return err
    }
    
    // Check string length
    if err := ai.ValidateStringLength(o.ID, 1, 50); err != nil {
        return err
    }
    
    // Check enum values
    if err := ai.ValidateOneOf(o.Currency, "USD", "EUR", "GBP"); err != nil {
        return err
    }
    
    return nil
}

Parse Configuration

Full control with ParseConfig:

config := &ai.ParseConfig{
    MaxRetries:     5,              // retry up to 5 times
    ValidateOutput: true,           // run struct validators
    IncludeSchema:  true,           // include JSON schema in prompt
    StrictMode:     false,          // allow extra fields
    Timeout:        30 * time.Second, // timeout per attempt
}

err := ai.Claude().ParseIntoWithConfig("Extract...", &result, config)

Common Extractors

Classify

Classify text into categories:

labels := []string{"spam", "not_spam", "unsure"}
result, err := ai.Classify(ai.Claude(), "Win a free iPhone now!!!", labels)

fmt.Printf("Label: %s (%.0f%% confidence)\n", result.Label, result.Confidence*100)
fmt.Printf("Reason: %s\n", result.Reasoning)
// Label: spam (95% confidence)
// Reason: Contains common spam indicators like "free" and "Win"

Extract Entities

Extract named entities:

entities, err := ai.ExtractEntities(ai.Claude(),
    "Apple CEO Tim Cook announced the iPhone 15 in Cupertino",
    []string{"person", "company", "product", "location"})

for _, e := range entities {
    fmt.Printf("%s: %s (%s)\n", e.Type, e.Name, e.Value)
}
// company: Apple
// person: Tim Cook
// product: iPhone 15
// location: Cupertino

Analyze Sentiment

Sentiment analysis:

sentiment, err := ai.AnalyzeSentiment(ai.Claude(),
    "I absolutely love this product! Best purchase ever!")

fmt.Printf("Sentiment: %s (%.2f)\n", sentiment.Label, sentiment.Score)
fmt.Printf("Emotions: %v\n", sentiment.Emotions)
// Sentiment: positive (0.95)
// Emotions: [joy excitement satisfaction]

Defining Good Schemas

Use JSON Tags

type Event struct {
    Name      string    `json:"name"`               // required
    Date      string    `json:"date"`               // required
    Location  string    `json:"location,omitempty"` // optional
    Attendees int       `json:"attendees,omitempty"` // optional
}

Add Descriptions

Use the desc tag for field descriptions:

type Analysis struct {
    Summary    string   `json:"summary" desc:"Brief 1-2 sentence summary"`
    KeyPoints  []string `json:"key_points" desc:"3-5 main points"`
    Sentiment  string   `json:"sentiment" desc:"positive, negative, or neutral"`
    Confidence float64  `json:"confidence" desc:"0.0 to 1.0"`
}

Nested Structures

type Article struct {
    Title    string   `json:"title"`
    Author   Author   `json:"author"`
    Sections []Section `json:"sections"`
}

type Author struct {
    Name  string `json:"name"`
    Email string `json:"email,omitempty"`
}

type Section struct {
    Heading string `json:"heading"`
    Content string `json:"content"`
}

Complete Example

package main

import (
    "fmt"
    ai "gopkg.in/dragon-born/go-llm.v1"
)

type Recipe struct {
    Name        string       `json:"name"`
    PrepTime    int          `json:"prep_time" desc:"Preparation time in minutes"`
    CookTime    int          `json:"cook_time" desc:"Cooking time in minutes"`
    Servings    int          `json:"servings"`
    Ingredients []Ingredient `json:"ingredients"`
    Steps       []string     `json:"steps"`
}

type Ingredient struct {
    Name   string  `json:"name"`
    Amount float64 `json:"amount"`
    Unit   string  `json:"unit"`
}

func (r *Recipe) Validate() error {
    if r.Name == "" {
        return fmt.Errorf("recipe name is required")
    }
    if len(r.Ingredients) == 0 {
        return fmt.Errorf("at least one ingredient is required")
    }
    if len(r.Steps) == 0 {
        return fmt.Errorf("at least one step is required")
    }
    return nil
}

func main() {
    prompt := `Extract a recipe from this text:
    
    Grandma's Chocolate Chip Cookies
    
    Mix 2 cups flour, 1 cup sugar, and 1 cup chocolate chips.
    Add 2 eggs and 1 cup butter. Mix well.
    Bake at 350°F for 12 minutes. Makes 24 cookies.`

    var recipe Recipe
    err := ai.Claude().
        ThinkLow().
        IntoWithRetry(prompt, &recipe, 3)

    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }

    fmt.Printf("Recipe: %s\n", recipe.Name)
    fmt.Printf("Prep: %d min, Cook: %d min\n", recipe.PrepTime, recipe.CookTime)
    fmt.Printf("Servings: %d\n", recipe.Servings)
    fmt.Println("\nIngredients:")
    for _, ing := range recipe.Ingredients {
        fmt.Printf("  - %.1f %s %s\n", ing.Amount, ing.Unit, ing.Name)
    }
    fmt.Println("\nSteps:")
    for i, step := range recipe.Steps {
        fmt.Printf("  %d. %s\n", i+1, step)
    }
}

Best Practices

  1. Use clear field names - The model uses these to understand structure
  2. Add descriptions - Use desc tag for complex fields
  3. Mark optional fields - Use omitempty for non-required fields
  4. Implement validation - Catch errors early with StructValidator
  5. Set reasonable retries - 3 is usually enough, more for complex schemas
  6. Keep schemas flat - Deeply nested structures are harder to parse correctly

Clone this wiki locally